From 9a14be4ee4c8cdcb9886d1e8b8a104361e60cade Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?=
<76204082+iamsedatozturk@users.noreply.github.com>
Date: Thu, 24 Sep 2026 16:29:53 +0300
Subject: [PATCH] =?UTF-8?q?Mcp=20problemlerini=20giderildi,=20Wizard=20sil?=
=?UTF-8?q?me=20i=C5=9Flemi=20de=C4=9Fi=C5=9Ftirildi.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../lowcode-reference.instructions.md | 2 +-
.github/instructions/lowcode.instructions.md | 33 +-
README.md | 16 +-
.../Dtos/McpSchemaDtos.cs | 7 +-
.../IMcpDevelopAppService.cs | 9 +-
.../McpDevelopAppService.cs | 68 +++-
.../Sozsoft.Mcp.Application/McpLoadOptions.cs | 4 +-
.../McpSchemaAppService.cs | 4 +-
.../Sozsoft.Mcp.Domain.Shared/McpConsts.cs | 7 +
.../McpErrorCodes.cs | 2 +
.../Sozsoft.Mcp.Domain.Shared/McpToolNames.cs | 5 +-
.../Tools/McpDevelopTools.cs | 25 +-
.../Tools/McpSchemaTools.cs | 13 +-
.../Wizard/IListFormWizardAppService.cs | 5 +-
.../ListForms/Wizard/WizardSeedFileDto.cs | 49 +++
.../ListForms/ListFormWizardAppService.cs | 49 ++-
.../ListForms/WizardRecordCleaner.cs | 43 ++
.../ListForms/WizardTableCleaner.cs | 373 ++++++++++++++++++
.../Migrations/LanguagesData.json | 96 +++++
.../Enums/WizardRecordTypeEnum.cs | 19 +
.../PlatformHttpApiHostModule.cs | 27 +-
claude.md | 7 +-
ui/src/constants/permission.constant.ts | 2 +
ui/src/proxy/admin/wizard/models.ts | 33 ++
ui/src/services/wizard.service.ts | 18 +-
.../JsonRowOpDialogEditForm.tsx | 4 +-
.../listForm/wizard/WizardFileManager.tsx | 142 ++++++-
.../listForm/wizard/WizardStepSubForms.tsx | 4 +-
.../listForm/wizard/WizardStepWidgets.tsx | 4 +-
29 files changed, 1010 insertions(+), 60 deletions(-)
create mode 100644 api/src/Sozsoft.Platform.Application/ListForms/WizardTableCleaner.cs
diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md
index ddfab24f..6f4a5e76 100644
--- a/.github/instructions/lowcode-reference.instructions.md
+++ b/.github/instructions/lowcode-reference.instructions.md
@@ -1265,7 +1265,7 @@ soneklerini alır.
| **Developer Kit › ListForm** | `App.Listforms.Listform` | `Sas_H_ListForm` · `Sas_H_ListFormField` · `Sas_H_ListFormWorkflow` · `Sas_H_ListFormCustomization` · `Sas_H_ListFormImport` | host | Ekran tanımlarının kendisi. Bu dosyada anlatılan bütün JSON kolonlarının canlı hâli; düzenleyicisi `/admin/listform/edit/{kod}`. |
| **Developer Kit › Intranet Widgets** | `App.DeveloperKit.IntranetWidgets` | `Sas_T_IntranetWidget` | tenant | Intranet panosunda hangi widget'ın hangi kolonda (`Column`), hangi sırada (`Order`) ve hangi yetkiyle (`PermissionName`) çizileceği; `SourceType` `Builtin` (koddaki `Code`) ya da `Custom` (`ComponentName` ile bir custom component). Seed: `data/App.DeveloperKit.IntranetWidgets.json`. |
| **Developer Kit › MCP Servers** | `App.DeveloperKit.McpServers` | `Sas_H_McpServer` | host | `/mcp` ucunun kiracı bazlı sözleşmesi: `Name`, `Url`, `Tenants`, `AllowedTools` (boşsa tüm araçlar, doluysa yalnızca seçilenler), `MaxRows`. Yetkiler: ekran `App.DeveloperKit.McpServers`, erişim ve okuma `.Invoke`, her değişiklik `.Write` (kiracıda satır yazma, host'ta ayrıca tablo/ekran/bileşen geliştirme) — arkasındaki ekran C/U/D, Wizard ve Custom Components yetkileri ayrıca istenir. Kiracı veri okur ve yazar, geliştirme yapamaz (`TenantTools`: şema araçları, `query_list`, `run_select`, `insert_row`/`update_row`/`delete_row`); ham SQL `App.SqlQueryManager` ister; MCP yetki vermez. Kayıtlar `data/App.DeveloperKit.McpServers.json`'a senkronlanır. Ayrıntı: `README.md` §13.9. |
-| **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | `configs/seeds/{kapsam}/wizard/*.json` | host | Wizard seed dosyalarının kart/liste görünümü: menü üst düğümü ve URL, **bağımlılıklar**, düzenleme, export (bağımlılık kapanışı) / import ve **silme** (`WizardRecordCleaner`: ekran ve izlenen kayıtları kaldırılır, başka ekran kullanıyorsa engellenir, sonuç raporlanır — `lowcode` §4.5). Sihirbazın kendisi `/admin/listform/wizard`. |
+| **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | `configs/seeds/{kapsam}/wizard/*.json` | host | Wizard seed dosyalarının kart/liste görünümü: menü üst düğümü ve URL, **bağımlılıklar**, düzenleme, export (bağımlılık kapanışı) / import ve **silme** (`WizardRecordCleaner`: ekran, veri aynası ve izlenen kayıtları kaldırılır, başka ekran kullanıyorsa engellenir, sonuç raporlanır; onay diyaloğu ekranın tablosunu sorar, seçilirse `WizardTableCleaner` tabloyu scripti ve crud dosyasıyla düşürür — `lowcode` §4.5). Sihirbazın kendisi `/admin/listform/wizard`. |
| **Forum Management › Categories / Topics / Posts** | `App.ForumManagement.Categories` · `.Topics` · `.Posts` | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Forumun yönetim tarafı: kategori (seed `data/App.ForumManagement.Categories.json`), konu (kategori lookup'ı, sayaçlar, sabitle/kilitle/çözüldü bayrakları), gönderi (konu lookup'ı, kabul edilen yanıt). Kullanıcı tarafı Administration'daki `/admin/forum`. |
> Menü kodu ile yetki adı **her zaman aynı değildir**: Wizard Manager'da kod
diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md
index 880d5c9d..9e5e10e3 100644
--- a/.github/instructions/lowcode.instructions.md
+++ b/.github/instructions/lowcode.instructions.md
@@ -504,10 +504,11 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz.
| `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz |
| `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` |
| `deploy_wizard` | Wizard cevabından ekranı, menüyü, yetkileri, dil anahtarlarını ve seed dosyasını üretir | 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). İstek demo veri taşıyorsa `SeedData: true` (§0.10.1) |
-| `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler | Bağımlı wizard varsa reddeder (§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) |
| `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + resim içerikleri) | Yarım kalan talebe dönmek için; `App.DevRequests` ister |
| `append_dev_request_note` | Talep açıklamasının sonuna HTML ekler | Sonuç notu için; `Description`'ı `update_row` ile yazma (§0.11) |
+| `add_dev_request_subtasks` | `SubTasks` listesine tamamlanmış satırlar ekler | Atanan = bağlanan kullanıcı, bitiş = bugün (sunucu yazar); `SubTasks`'ı `update_row` ile yazma (§0.11) |
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü, yetki tanımı, ekran sorgusu (List) ya da bileşen (Custom), seed kapanışı | `passed` yalnızca engelleyici bir kontrol kalırsa `false`; `seed_closure` engellemez ama rapora yazılır (§0.11) |
| `list_components` · `get_component` · `save_component` · `delete_component` | Custom component okuma/yazma (ada göre upsert) | `save_component` visual designer bileşenini reddeder — yalnız Code modu (§7.1.1) |
@@ -585,8 +586,10 @@ talebin metnini döndürür. Başlık birden fazla talebe uyuyorsa Id ister. `St
| 5b. Kaldı | `update_row` → `In Progress` + sonuç kaydı; düzelt ve 4'e dön ya da bırak | In Progress |
**Sonuç kaydı** şema değiştirmeden mevcut alanlara yazılır:
-- `SubTasks` — her üretilen artefakt bir satır: `{ "id": "", "title": "deploy_wizard: Mrp.Orders.json", "assignee": "", "dueDate": null, "completed": true }`.
- Var olan satırları koru, sona ekle (alan JSON dizisi metnidir).
+- `SubTasks` — her üretilen artefakt bir satır, `add_dev_request_subtasks` ile yalnızca başlık
+ verilerek (`"deploy_wizard: Mrp.Orders.json"`). Araç satırı tamamlanmış yazar, atanan kişiyi
+ bağlanan kullanıcı, bitiş tarihini bugün yapar ve var olan satırları korur. `SubTasks`
+ `update_row` ile yazılmaz.
- `Description` — `append_dev_request_note` ile sonuna `MCP
` başlığıyla `verify_wizard`
özeti: geçen/kalan kontroller, `seed_closure` uyarıları. `Description` hiçbir zaman `update_row`
ile yazılmaz — base64 görselleri taşır ve yeniden yazmak onları siler.
@@ -1476,6 +1479,23 @@ modül, veri kaynağı ve admin dışındaki rollerin yetki atamaları korunur
kullanılırlar; silinip üretilseler menü sırası ve rol atamaları kaybolurdu. Wizard'ın yarattığı
modül `InsertedRecords.ModuleIds` ile izlenir.
+Silme yolunda ekranın veri aynası (`ListForm.SeedFilePath` → `data/{ListFormCode}.json` ya da
+klasörü) ekranla birlikte silinir; ekransız uygulanamaz.
+
+**Ekranın tablosu** wizard'ın ürettiği bir kayıt değildir (tasarımcı ya da `create_table` ile
+ayrıca kurulur, `InsertedRecords`'ta yoktur) ve **kendiliğinden silinmez**. Silme onayı önce
+`DeleteAnalysis` ile adayı sorar (`WizardTableCleaner`): yalnızca `Table` tipli ekranın tablosu
+aday olur ve iki koşulla seçilebilir — kapsamdaki `{sql|postgres}/object/{Tablo}.sql` bir
+`CREATE TABLE` scriptidir (platform tabloları bu klasörde durmaz; scriptsiz tablo `NotManaged`) ve
+tabloyu başka bir ekran (sorgu/C/U/D komutu ya da bu ekranı alt form olarak kullanan üst ekran),
+Custom Endpoint SQL'i ya da custom component veri kaynağı kullanmaz (`InUse`). Kullanıcı onay
+diyaloğunda tabloyu seçerse (`WizardDeleteInput.DropTables`) kayıt temizliğinden **sonra**
+`DROP TABLE` çalışır, ardından tablonun CRUD endpoint'leri ve seed dosyaları
+(`{sql|postgres}/object/{Tablo}.sql`, `crud/{Tablo}.json`) silinir. Düşürme hatası (ör. yabancı
+anahtar) silmeyi durdurmaz: tablo ve scripti yerinde kalır, rapor `Failed` + mesaj gösterir.
+Tablo düşürmek `App.SqlQueryManager` yetkisi ister; wizard silme yetkisi tek başına yetmez.
+Düzenleme (`Redeploy`) yolu tabloya hiç dokunmaz.
+
### 4.6 Alt formlar ve widget'lar
**`SubForms` (`SubFormDto`)** — ana kaydın altında sekme olarak açılan ekranlar:
@@ -4057,9 +4077,10 @@ tedarikçi lookup'ı + iki kademeli onay).
Endpoint…) ekranın `data/` dosyasına yazıldı; `SeedFilePath`'i olmayan ekranın kaydı için
`sql/execute/` altında geri getirici prosedür var (§9.14).
- [ ] Export zip'i başka bir ortamda açıldığında ekran ayağa kalkıyor.
-- [ ] Geri alma yolu yazılı: Wizard Manager'dan ya da MCP `delete_wizard` ile sil → dosya ve
- `InsertedRecords` içindeki kullanılmayan kayıtlar gider (§4.5). Dosyayı diskten silmek
- kayıtları temizlemez.
+- [ ] Geri alma yolu yazılı: Wizard Manager'dan ya da MCP `delete_wizard` ile sil → dosya,
+ veri aynası ve `InsertedRecords` içindeki kullanılmayan kayıtlar gider; tablo yalnızca
+ onayda seçilirse (MCP: `dropTables: true`) scripti ve crud dosyasıyla düşer (§4.5).
+ Dosyayı diskten silmek kayıtları temizlemez.
Prompt ile üretim yaptıysan ek olarak:
diff --git a/README.md b/README.md
index 02839f4d..29c66522 100644
--- a/README.md
+++ b/README.md
@@ -1358,7 +1358,11 @@ ardışık düzeninden sonra bağlanır ve açıkça bearer şemasına bağlıd
sayfasına 302 ile değil, 401 ile döner); kiracı token talebinden ya da `__tenant` başlığından
çözülür. Taşıyıcı **durumsuz** kiptedir
(`HttpServerSessionMode.Stateless`): her istek kendi ABP kapsamında koşar, ters vekil
-arkasında oturum yapışkanlığı gerekmez.
+arkasında oturum yapışkanlığı gerekmez. `/mcp` istek düzeyindeki unit of work'ün **dışındadır**
+(`AbpAspNetCoreUnitOfWorkOptions.IgnoredUrls`): cevap SSE ile aktığı için istek UoW'si sonuç
+istemciye gittikten sonra commit edilir ve bağlantı kapanınca geri alınırdı — araç başarılı
+görünür, EF kayıtları (ListForm, menü, yetki) kaybolur, DDL ve seed dosyası kalırdı. Her MCP
+servis çağrısı kendi UoW'sini sonuç dönmeden tamamlar.
**Kiracı bazlı sözleşme.** `Sas_H_McpServer` tablosundaki kayıt (ekran:
`App.DeveloperKit.McpServers`) hangi araçların yayınlandığını ve sorguların hangi satır tavanıyla koşacağını
@@ -1382,7 +1386,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| Araç | İş |
| --- | --- |
-| `server_info` | Bağlantının kiracısı, açık araçlar, veri sözlüğünün tazelik bilgisi. Her zaman açıktır. |
+| `server_info` | Bağlantının bağlamı (`scope`: `host` / `tenant`; kiracı adı `server.tenantName`), açık araçlar, veri sözlüğünün tazelik bilgisi. Sözlüğün `dictionaryKey` alanı (host'ta `Sozsoft`) sözlüğün yazıldığı anahtardır, kiracı bağlamını göstermez. Her zaman açıktır. |
| `list_lists` | Listeler: kod, başlık, açıklama, arkasındaki tablo. |
| `describe_list` | Bir listenin sütunları: alan adı, görünen başlık, SQL tipi, lookup ilişkisi, `isSensitive`. |
| `search_schema` | Liste ve sütun adları/başlıkları üzerinde aksan ve büyük/küçük harf duyarsız arama. |
@@ -1395,10 +1399,11 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `{kapsam}/sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. |
| `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. |
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. `replaceFileName` doluysa güncellemedir (eski dosya ve ürettiği kayıtlar silinip yeniden üretilir). İ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 — ardından satırlar `insert_row` ile eklenir; `ListFormSeedDataSynchronizer` onları `configs/seeds/host/data/` altına yazar, migrate ekranla birlikte geri yükler (`lowcode.instructions.md` §0.10.1). |
-| `delete_wizard` | Wizard Manager'daki silmeyle aynı: yalnızca `InsertedRecords`, bağımlı wizard varsa reddedilir. |
+| `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). `App.DevRequests` geliştirme döngüsünün test adımıdı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. |
| `get_dev_request` | Talebi durumuna dokunmadan, `start_dev_request` ile aynı biçimde (metin + resim içerikleri) okur; yarım kalan talebe dönmek için. `App.DevRequests` ister. |
+| `add_dev_request_subtasks` | Talebin `SubTasks` listesine tamamlanmış satırlar ekler: model yalnızca başlığı verir, atanan kişi bağlanan kullanıcının adı (`userName`), bitiş tarihi bugün (`yyyy-MM-dd`) olarak sunucuda yazılır; var olan satırlar korunur. `.Write` + `App.DevRequests.Update` ister. |
| `append_dev_request_note` | Talep açıklamasının sonuna HTML ekler, var olan metne ve görsellere dokunmaz. Açıklama base64 görsel taşıdığı için `update_row` ile yeniden yazılmaz. `.Write` + `App.DevRequests.Update` ister. |
**Prompt'lar** (`McpPromptNames`, `McpDevelopPrompts`). İstemcide komut olarak görünür; yalnızca
@@ -1446,7 +1451,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay
| --- | --- | --- | --- |
| `App.DeveloperKit.McpServers` | Host, kiracı | MCP Servers ekranını görme (`.Create/.Update/.Delete/.Export/.Import` ekran işlemleri) | `/mcp` ucuna erişim vermez |
| `App.DeveloperKit.McpServers.Invoke` | Host, kiracı | `/mcp` bağlantısı ve bütün okuma araçları: `server_info`, `list_lists`, `describe_list`, `search_schema`, `query_list`; host'ta ayrıca `get_dev_guide`, `list_wizards`, `get_wizard`, `verify_wizard`, `list_components`, `get_component` | Hiçbir şey yazmaz. Okuma da ekran yetkisine bağlıdır: `R` yetkisi olmayan liste görünmez, sorgulanamaz. Ham SQL (`run_select`, `list_queries`, `run_query`) ayrıca `App.SqlQueryManager` ister |
-| `App.DeveloperKit.McpServers.Write` | Host, kiracı | MCP üzerinden her değişiklik. **Kiracıda:** `insert_row`, `update_row`, `delete_row`. **Host'ta:** bunlara ek olarak `create_table`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `start_dev_request`, `append_dev_request_note` | Kendi başına bir şey yazdırmaz: satır araçları ekranın `C`/`U`/`D` yetkisini, geliştirme araçları Wizard / Custom Components / SQL Query Manager / `App.DevRequests.Update` yetkisini ayrıca ister. Kiracıda geliştirme araçlarını **açmaz** — kiracı bağlamında bu araçlar yetkiden bağımsız kapalıdır (`McpToolNames.TenantTools`). `Invoke` olmadan işe yaramaz |
+| `App.DeveloperKit.McpServers.Write` | Host, kiracı | MCP üzerinden her değişiklik. **Kiracıda:** `insert_row`, `update_row`, `delete_row`. **Host'ta:** bunlara ek olarak `create_table`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `start_dev_request`, `append_dev_request_note`, `add_dev_request_subtasks` | Kendi başına bir şey yazdırmaz: satır araçları ekranın `C`/`U`/`D` yetkisini, geliştirme araçları Wizard / Custom Components / SQL Query Manager / `App.DevRequests.Update` yetkisini ayrıca ister. Kiracıda geliştirme araçlarını **açmaz** — kiracı bağlamında bu araçlar yetkiden bağımsız kapalıdır (`McpToolNames.TenantTools`). `Invoke` olmadan işe yaramaz |
**Araç → gereken yetkiler** (Invoke hepsinde aranır, tabloda tekrar yazılmadı)
@@ -1461,7 +1466,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay
| `list_wizards`, `get_wizard` | — | — | `App.Listforms.Wizard` |
| `verify_wizard` | — | — | `App.Listforms.Wizard`; seed kapanışı için `.Export` (yoksa o kontrol atlanır, doğrulama düşmez) |
| `deploy_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Create` (yeni) / `.Update` (`replaceFileName`) |
-| `delete_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Delete` |
+| `delete_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Delete`; `dropTables` için ayrıca `App.SqlQueryManager` |
| `create_table` | — | `.Write` | `App.SqlQueryManager` |
| `list_components`, `get_component` | — | — | `App.DeveloperKit.CustomComponents` |
| `save_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Create` (yeni) / `.Update` |
@@ -1469,6 +1474,7 @@ yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ay
| `start_dev_request` | — | `.Write` | `App.DevRequests.Update` |
| `get_dev_request` | — | — | `App.DevRequests` |
| `append_dev_request_note` | — | `.Write` | `App.DevRequests.Update` |
+| `add_dev_request_subtasks` | — | `.Write` | `App.DevRequests.Update` |
Kaynak: MCP kapısı `McpToolNames.RequiredPermissions`, kiracı kümesi `McpToolNames.TenantTools`,
arkadaki servisin attribute ile istediği yetkiler `McpToolNames.ServicePermissions`. `Invoke` her MCP
diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs
index 5dd7884f..1f026c15 100644
--- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs
+++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs
@@ -36,7 +36,12 @@ public class McpSchemaInfoDto
public DateTime? RefreshedAt { get; set; }
public int FormCount { get; set; }
public int FieldCount { get; set; }
- public string? TenantName { get; set; }
+
+ ///
+ /// Sozlugun yazildigi anahtar (prosedurun @TenantName'i). Host'ta da dolu gelir;
+ /// istegin kiraci baglamini gostermez — o server_info'nun scope alanidir.
+ ///
+ public string? DictionaryKey { get; set; }
public string? DefinitionCultureName { get; set; }
public string? UiCultureName { get; set; }
diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs
index c329c6b6..952603bd 100644
--- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs
+++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs
@@ -25,7 +25,8 @@ public interface IMcpDevelopAppService : IApplicationService
/// Deploy edilen wizard'in ozeti.
Task DeployWizardAsync(McpWizardDeployRequestDto input);
- Task DeleteWizardAsync(string fileName);
+ /// Wizard'i siler; acikken ekranin secilebilir tablosunu da dusurur.
+ Task DeleteWizardAsync(string fileName, bool dropTables);
/// Deploy edilmis ekrani veri yazmadan dogrular.
Task VerifyWizardAsync(string fileName);
@@ -42,6 +43,12 @@ public interface IMcpDevelopAppService : IApplicationService
///
Task AppendDevRequestNoteAsync(string request, string html);
+ ///
+ /// Talebin SubTasks listesine tamamlanmis satirlar ekler; atanan kisi cagiran kullanici,
+ /// bitis tarihi bugundur.
+ ///
+ Task AddDevRequestSubTasksAsync(string request, List titles);
+
Task> GetComponentsAsync(string? search);
Task GetComponentAsync(string name);
diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs
index 9d6a1314..4b2370c4 100644
--- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs
+++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
@@ -49,6 +50,9 @@ public class McpDevelopAppService(
private const string DevRequestUpdatePermission = PlatformConsts.AppCodes.DevRequests + ".Update";
+ /// Talep ekranindaki alt gorev tarih kutusunun okudugu bicim.
+ private const string SubTaskDateFormat = "yyyy-MM-dd";
+
/// verify_wizard cevabindaki kontrol adlari.
private static class VerifyChecks
{
@@ -162,11 +166,22 @@ public class McpDevelopAppService(
return files.FirstOrDefault(file => string.Equals(file.WizardName, wizard.WizardName?.Trim(), StringComparison.OrdinalIgnoreCase));
}
- public async Task DeleteWizardAsync(string fileName)
+ ///
+ /// Ekranin tablosu ancak acikken ve Wizard Manager'daki onayla ayni
+ /// kosullarda (secilebilir aday) dusurulur; onayi kullanicidan almak istemcinin isidir.
+ ///
+ public async Task DeleteWizardAsync(string fileName, bool dropTables)
{
await EnsureToolAllowedAsync(McpToolNames.DeleteWizard);
- return await wizardAppService.DeleteFile(fileName);
+ var input = new WizardDeleteInput { FileName = fileName };
+ if (dropTables)
+ {
+ var analysis = await wizardAppService.DeleteAnalysis(fileName);
+ input.DropTables = [.. analysis.Tables.Where(t => t.CanDrop).Select(t => t.TableName)];
+ }
+
+ return await wizardAppService.DeleteFile(input);
}
///
@@ -280,6 +295,55 @@ public class McpDevelopAppService(
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
}
+ ///
+ /// Atanan kisi ve tarih modelin dikkatine birakilmaz: satiri yazan cagiran kullanicidir
+ /// (ekranin atanan listesi kullanici adiyla calisir), bitis tarihi yazildigi gundur.
+ /// Var olan satirlara dokunulmaz.
+ ///
+ public async Task AddDevRequestSubTasksAsync(string request, List titles)
+ {
+ await EnsureToolAllowedAsync(McpToolNames.AddDevRequestSubTasks);
+ await AuthorizationService.CheckAsync(DevRequestUpdatePermission);
+
+ var items = (titles ?? []).Select(title => title?.Trim()).Where(title => !string.IsNullOrEmpty(title)).ToList();
+ if (items.Count == 0)
+ {
+ throw new UserFriendlyException(L[McpErrorCodes.DevRequestSubTasksRequired]);
+ }
+
+ var devRequest = await FindDevRequestAsync(request);
+
+ JsonArray subTasks;
+ try
+ {
+ subTasks = string.IsNullOrWhiteSpace(devRequest.SubTasks)
+ ? []
+ : JsonNode.Parse(devRequest.SubTasks) as JsonArray ?? throw new JsonException();
+ }
+ catch (JsonException)
+ {
+ throw new UserFriendlyException(L[McpErrorCodes.DevRequestSubTasksInvalid, devRequest.Title ?? devRequest.Id.ToString()]);
+ }
+
+ var assignee = CurrentUser.UserName ?? string.Empty;
+ var dueDate = Clock.Now.ToString(SubTaskDateFormat, CultureInfo.InvariantCulture);
+
+ foreach (var title in items)
+ {
+ subTasks.Add(new JsonObject
+ {
+ ["id"] = GuidGenerator.Create().ToString(),
+ ["title"] = title,
+ ["assignee"] = assignee,
+ ["dueDate"] = dueDate,
+ ["completed"] = true
+ });
+ }
+
+ devRequest.SubTasks = subTasks.ToJsonString();
+ await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
+ }
+
private static McpDevRequestDto ToDto(DevRequest devRequest)
{
var (description, images) = McpDevRequestImages.Extract(devRequest.Description);
diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpLoadOptions.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpLoadOptions.cs
index a6a5f18c..49291cb1 100644
--- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpLoadOptions.cs
+++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpLoadOptions.cs
@@ -15,8 +15,6 @@ namespace Sozsoft.Mcp;
///
internal static class McpLoadOptions
{
- private static readonly string Delimiter = PlatformConsts.MultiValueDelimiter.ToString();
-
public static string? ToSort(string? json)
=> Convert(json, item => $"{Selector(item)} {Direction(item)}");
@@ -54,7 +52,7 @@ internal static class McpLoadOptions
.Select(map)
.ToList();
- return parts.Count == 0 ? null : string.Join(Delimiter, parts);
+ return parts.Count == 0 ? null : string.Join(PlatformConsts.MultiValueDelimiter, parts);
}
private static string Selector(JsonElement item) => Text(item, "selector");
diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs
index a0a88a9f..bee9db5f 100644
--- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs
+++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs
@@ -61,7 +61,7 @@ public class McpSchemaAppService(
return new McpSchemaInfoDto
{
Available = false,
- TenantName = dictionary.ResolveTenantName(),
+ DictionaryKey = dictionary.ResolveTenantName(),
Message = L[McpErrorCodes.DictionaryNotFound, McpConsts.DictionaryProcedureName]
};
}
@@ -73,7 +73,7 @@ public class McpSchemaAppService(
RefreshedAt = snapshot.RefreshedAt,
FormCount = snapshot.FormCount,
FieldCount = snapshot.FieldCount,
- TenantName = snapshot.TenantName,
+ DictionaryKey = snapshot.TenantName,
DefinitionCultureName = snapshot.DefinitionCultureName,
UiCultureName = snapshot.UiCultureName
};
diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs
index af99641b..6b8381ee 100644
--- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs
+++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs
@@ -15,6 +15,13 @@ public static class McpConsts
///
public const string DefaultDictionaryTenantName = "Sozsoft";
+ /// server_info cevabindaki istek baglami.
+ public static class Scopes
+ {
+ public const string Host = "host";
+ public const string Tenant = "tenant";
+ }
+
/// Sunucu kaydinda satir tavani verilmediginde (ya da sifir oldugunda) uygulanan deger.
public const int DefaultMaxRows = 200;
diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs
index 15d51ded..4fe49c2e 100644
--- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs
+++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs
@@ -50,6 +50,8 @@ public static class McpErrorCodes
public const string DevRequestAmbiguous = Prefix + "DevRequestAmbiguous";
public const string DevRequestNotReady = Prefix + "DevRequestNotReady";
public const string DevRequestNoteRequired = Prefix + "DevRequestNoteRequired";
+ public const string DevRequestSubTasksRequired = Prefix + "DevRequestSubTasksRequired";
+ public const string DevRequestSubTasksInvalid = Prefix + "DevRequestSubTasksInvalid";
public const string VisualDesignerComponent = Prefix + "VisualDesignerComponent";
public const string TableColumnsRequired = Prefix + "TableColumnsRequired";
public const string UnsupportedColumnType = Prefix + "UnsupportedColumnType";
diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs
index fbf822d5..1c3909a8 100644
--- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs
+++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs
@@ -32,6 +32,7 @@ public static class McpToolNames
public const string StartDevRequest = "start_dev_request";
public const string GetDevRequest = "get_dev_request";
public const string AppendDevRequestNote = "append_dev_request_note";
+ public const string AddDevRequestSubTasks = "add_dev_request_subtasks";
public const string ListComponents = "list_components";
public const string GetComponent = "get_component";
public const string SaveComponent = "save_component";
@@ -54,6 +55,7 @@ public static class McpToolNames
StartDevRequest,
GetDevRequest,
AppendDevRequestNote,
+ AddDevRequestSubTasks,
ListComponents,
GetComponent,
SaveComponent,
@@ -105,6 +107,7 @@ public static class McpToolNames
[DeleteWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[StartDevRequest] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[AppendDevRequestNote] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
+ [AddDevRequestSubTasks] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[SaveComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[DeleteComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
};
@@ -122,7 +125,7 @@ public static class McpToolNames
[GetWizard] = [PlatformConsts.AppCodes.Listforms.Wizard],
[VerifyWizard] = [PlatformConsts.AppCodes.Listforms.Wizard],
[DeployWizard] = [PlatformConsts.AppCodes.Listforms.Wizard],
- [DeleteWizard] = [PlatformConsts.AppCodes.Listforms.Wizard, PlatformConsts.AppCodes.Listforms.Wizards.Delete],
+ [DeleteWizard] = [PlatformConsts.AppCodes.Listforms.Wizard, PlatformConsts.AppCodes.Listforms.Wizards.Delete, PlatformConsts.AppCodes.SqlQueryManagers.Default],
};
///
diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs
index a1f0e33f..aacf9e4b 100644
--- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs
+++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs
@@ -1,6 +1,6 @@
-using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using ModelContextProtocol.Protocol;
@@ -73,11 +73,13 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
=> await developAppService.DeployWizardAsync(new McpWizardDeployRequestDto { Wizard = wizard, ReplaceFileName = replaceFileName });
[McpServerTool(Name = McpToolNames.DeleteWizard, Destructive = true, Idempotent = true)]
- [Description("Bir wizard'i ve yalnizca onun urettigi kayitlari siler; baska yerde kullanilan kayitlar korunur ve raporda gosterilir. Bagimli wizard'i olan bir wizard silinemez, once bagimlilar silinir.")]
+ [Description("Bir wizard'i ve yalnizca onun urettigi kayitlari siler; baska yerde kullanilan kayitlar korunur ve raporda gosterilir. Ekranin veri aynasi (data/{ListFormCode}.json) ekranla birlikte gider. Bagimli wizard'i olan bir wizard silinemez, once bagimlilar silinir. Ekranin tablosu varsayilan olarak kalir; dropTables true ise tablo DROP edilir, CRUD endpoint'leri ve sql/object + crud seed dosyalari da silinir. Tablo yalnizca create_table ya da tablo tasarimcisiyla olusturulmussa (sql/object scripti varsa) ve baska ekran, Custom Endpoint ya da bilesen kullanmiyorsa dusurulur; aksi halde raporda neden korundugu yazar. Tablo silmek geri alinamaz ve App.SqlQueryManager yetkisi ister: dropTables true gondermeden once kullaniciya tablonun ve icindeki verinin silinecegini soyle ve acik onay al.")]
public async Task DeleteWizardAsync(
[Description("Silinecek wizard dosyasinin adi (list_wizards cikisindaki fileName).")]
- string fileName)
- => await developAppService.DeleteWizardAsync(fileName);
+ string fileName,
+ [Description("Ekranin tablosu da dusurulsun mu. Yalnizca kullanici acikca onayladiysa true.")]
+ bool dropTables = false)
+ => await developAppService.DeleteWizardAsync(fileName, dropTables);
[McpServerTool(Name = McpToolNames.VerifyWizard, ReadOnly = true)]
[Description("Deploy edilmis bir wizard'i veri yazmadan dogrular: menu kaydi (var mi, etkin mi, URL'si wizard'la ayni mi), menunun istedigi yetki tanimi, liste ekraninda ekranin sorgusu (cagiranin yetkisiyle tek satir), Custom yolunda bilesen (var mi, etkin mi, rotasi menuyle ayni mi) ve seed kapanisi (migrate ya da baska ortamda eksik kalacak bagimlilik). passed yalnizca blocking kontrollerden biri kalirsa false olur; seed_closure engellemez ama raporlanmalidir. deploy_wizard'dan sonra ve bir gelistirme talebini Staging durumuna almadan once cagir.")]
@@ -112,6 +114,18 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
return "ok";
}
+ [McpServerTool(Name = McpToolNames.AddDevRequestSubTasks, Destructive = false, Idempotent = false)]
+ [Description("Talebin Sub Tasks listesine uretilen her artefakt icin tamamlanmis bir satir ekler; atanan kisi baglanan kullanici, bitis tarihi bugun olarak sunucuda yazilir, var olan satirlar korunur. SubTasks alanini update_row ile yazma.")]
+ public async Task AddDevRequestSubTasksAsync(
+ [Description("Talebin Id'si (GUID) ya da tam basligi.")]
+ string request,
+ [Description("Eklenecek satir basliklari, ornegin [\"create_table: Hr_T_Certificate\", \"deploy_wizard: Certificates.json\"].")]
+ List titles)
+ {
+ await developAppService.AddDevRequestSubTasksAsync(request, titles);
+ return "ok";
+ }
+
///
/// Talep metni JSON olarak, gorseller ayri resim icerigi olarak doner; model resmi ancak bu
/// bicimde gorur, base64 metni olarak goremez.
@@ -122,7 +136,8 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
foreach (var image in devRequest.Images)
{
- content.Add(ImageContentBlock.FromBytes(Convert.FromBase64String(image.Data), image.MimeType));
+ // Veri zaten base64; cozup yeniden kodlamak buyuk gorsellerde bosuna bellek harcar.
+ content.Add(new ImageContentBlock { Data = Encoding.UTF8.GetBytes(image.Data), MimeType = image.MimeType });
}
return content;
diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs
index ea2cecb3..324dbbf4 100644
--- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs
+++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs
@@ -3,6 +3,7 @@ using System.ComponentModel;
using System.Threading.Tasks;
using ModelContextProtocol.Server;
using Sozsoft.Mcp.Dtos;
+using Volo.Abp.MultiTenancy;
namespace Sozsoft.Mcp.Tools;
@@ -12,17 +13,21 @@ namespace Sozsoft.Mcp.Tools;
/// ekranda gordugu adlarla ayni olur.
///
[McpServerToolType]
-public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService)
+public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService, ICurrentTenant currentTenant)
{
[McpServerTool(Name = McpToolNames.ServerInfo, ReadOnly = true)]
- [Description("Bu baglantinin kiracisini, acik araclari ve veri sozlugunun tazelik bilgisini dondurur. Baska bir arac beklendigi gibi calismiyorsa once bunu cagir.")]
+ [Description("Bu baglantinin baglamini (scope: host ya da tenant; kiraci adi server.tenantName), acik araclari ve veri sozlugunun tazelik bilgisini dondurur. Baska bir arac beklendigi gibi calismiyorsa once bunu cagir.")]
public async Task ServerInfoAsync()
{
var config = await schemaAppService.GetServerConfigAsync();
var schema = await schemaAppService.GetSchemaInfoAsync();
var availableTools = await schemaAppService.GetAvailableToolsAsync();
- return new McpServerInfoResult(config, schema, availableTools);
+ return new McpServerInfoResult(
+ currentTenant.IsAvailable ? McpConsts.Scopes.Tenant : McpConsts.Scopes.Host,
+ config,
+ schema,
+ availableTools);
}
[McpServerTool(Name = McpToolNames.ListLists, ReadOnly = true)]
@@ -51,10 +56,12 @@ public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService)
=> await schemaAppService.SearchAsync(term, maxResults);
}
+/// Istegin baglami: host ya da tenant. Gelistirme araclari yalnizca host'ta aciktir.
/// Istegin kiracisina dusen sunucu kaydi; yoksa null.
/// Veri sozlugunun tazelik bilgisi.
/// Bu baglantida cagrilabilen araclar.
public sealed record McpServerInfoResult(
+ string Scope,
McpServerConfigDto? Server,
McpSchemaInfoDto Schema,
IReadOnlyList AvailableTools);
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs
index 2eb727cc..289178db 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs
@@ -9,7 +9,10 @@ public interface IListFormWizardAppService
Task Create(ListFormWizardDto input);
Task> GetFiles();
Task GetFile(string fileName);
- Task DeleteFile(string fileName);
+ Task DeleteFile(WizardDeleteInput input);
+
+ /// Silme onayinda sorulacak tablolari ve tabloyla birlikte gidecek seed dosyalarini dondurur.
+ Task DeleteAnalysis(string fileName);
/// Izin gruplarini iki dilli gorunen adlariyla dondurur.
Task> GetPermissionGroups();
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs
index bacae3dd..f8a7a1c9 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs
@@ -386,6 +386,9 @@ public class WizardDeleteLogItemDto
public WizardDeleteStatusEnum Status { get; set; }
public WizardDeleteReasonEnum Reason { get; set; }
public List UsedBy { get; set; } = [];
+
+ /// durumunda veritabaninin hata mesaji.
+ public string Message { get; set; }
}
/// Wizard silme raporu; silinen, korunan ve bulunamayan her kayit tek tek listelenir.
@@ -396,6 +399,52 @@ public class WizardDeleteResultDto
public List Items { get; set; } = [];
}
+///
+/// Silme onayinda gosterilen, wizard'in ekranina bagli tablo. Tablo wizard'in urettigi bir kayit
+/// degildir ( icinde yoktur); bu yuzden kendiliginden
+/// silinmez, kullanici onay diyalogunda secerse dusurulur.
+///
+public class WizardDeleteTableDto
+{
+ public string DataSourceCode { get; set; }
+ public string SchemaName { get; set; }
+ public string TableName { get; set; }
+
+ /// Tablo veri kaynaginda var mi; yoksa yalnizca seed dosyalari silinir.
+ public bool Exists { get; set; }
+
+ public long? RowCount { get; set; }
+
+ /// Tablo secilebilir mi; degilse neden , kullananlar .
+ public bool CanDrop { get; set; }
+
+ public WizardDeleteReasonEnum Reason { get; set; }
+ public List UsedBy { get; set; } = [];
+
+ /// Tabloyla birlikte silinecek seed dosyalari, kapsam klasorune gore goreli.
+ public List Files { get; set; } = [];
+}
+
+/// Silmeden once onay diyaloguna gelen analiz.
+public class WizardDeleteAnalysisDto
+{
+ public string FileName { get; set; }
+ public string WizardName { get; set; }
+ public List Tables { get; set; } = [];
+}
+
+public class WizardDeleteInput
+{
+ public string FileName { get; set; }
+
+ ///
+ /// Onay diyalogunda secilen tablolar (). Yalnizca
+ /// analizde olanlar dusurulur; bos ise hicbir tabloya
+ /// dokunulmaz.
+ ///
+ public List DropTables { get; set; } = [];
+}
+
/// Wizard seed dosyası özet bilgisi (yönetim listesi için).
public class WizardFileInfoDto
{
diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs
index 7c30293e..bcc7710d 100644
--- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs
+++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs
@@ -57,6 +57,7 @@ public class ListFormWizardAppService(
IListFormSeedDataApplier seedDataApplier,
ICrudEndpointSeedApplier crudEndpointSeedApplier,
WizardRecordCleaner recordCleaner,
+ WizardTableCleaner tableCleaner,
ILogger logger
) : PlatformAppService(), IListFormWizardAppService
{
@@ -77,6 +78,7 @@ public class ListFormWizardAppService(
private readonly IListFormSeedDataApplier _seedDataApplier = seedDataApplier;
private readonly ICrudEndpointSeedApplier _crudEndpointSeedApplier = crudEndpointSeedApplier;
private readonly WizardRecordCleaner _recordCleaner = recordCleaner;
+ private readonly WizardTableCleaner _tableCleaner = tableCleaner;
private readonly ILogger logger = logger;
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
@@ -776,10 +778,37 @@ public class ListFormWizardAppService(
[UnitOfWork]
[Authorize(AppCodes.Listforms.Wizards.Delete)]
- public async Task DeleteFile(string fileName)
+ [HttpPost("api/app/list-form-wizard/delete-file")]
+ public async Task DeleteFile(WizardDeleteInput input)
{
- await EnsureNoDependentWizardsAsync(fileName);
- return await DeleteFileInternalAsync(fileName, WizardDeleteMode.Delete);
+ Check.NotNull(input, nameof(input));
+
+ var dropTables = input.DropTables?.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t.Trim()).ToList() ?? [];
+
+ // Tablo dusurmek SQL Query Manager'in isidir; wizard silme yetkisi tek basina tablo silmez.
+ if (dropTables.Count > 0)
+ await AuthorizationService.CheckAsync(AppCodes.SqlQueryManagers.Default);
+
+ await EnsureNoDependentWizardsAsync(input.FileName);
+ return await DeleteFileInternalAsync(input.FileName, WizardDeleteMode.Delete, dropTables);
+ }
+
+ ///
+ /// Silme onayinda sorulacak tablolari dondurur. Tablo wizard'in urettigi bir kayit olmadigi icin
+ /// kendiliginden silinmez; kullanici burada gordugu adaylardan secer ().
+ ///
+ [Authorize(AppCodes.Listforms.Wizards.Delete)]
+ [HttpGet("api/app/list-form-wizard/delete-analysis")]
+ public async Task DeleteAnalysis(string fileName)
+ {
+ var seed = await GetFile(fileName);
+
+ return new WizardDeleteAnalysisDto
+ {
+ FileName = fileName,
+ WizardName = seed.Wizard?.WizardName ?? fileName,
+ Tables = await _tableCleaner.AnalyzeAsync(seed),
+ };
}
///
@@ -806,7 +835,7 @@ public class ListFormWizardAppService(
/// akibeti rapora ve loga yazilir (). Yetki kontrolu cagiran
/// uca aittir: silme yolunda Delete, duzenleme yolunda Update politikasi kullanilir.
///
- private async Task DeleteFileInternalAsync(string fileName, WizardDeleteMode mode)
+ private async Task DeleteFileInternalAsync(string fileName, WizardDeleteMode mode, List? dropTables = null)
{
var filePath = ResolveWizardSeedFilePath(fileName);
@@ -819,9 +848,21 @@ public class ListFormWizardAppService(
WizardName = seed?.Wizard?.WizardName ?? fileName,
};
+ // Tablolar ekran silinmeden once analiz edilir: kullanim kontrolu ekranin kendisini disarida
+ // birakir ve baglanti, veri kaynagi kaydi temizlenmeden once cozulur. Duzenleme yolunda
+ // tabloya hic dokunulmaz.
+ var tables = mode == WizardDeleteMode.Delete && seed != null
+ ? await _tableCleaner.AnalyzeAsync(seed)
+ : [];
+ var connections = await _tableCleaner.ResolveConnectionsAsync(
+ tables.Where(t => dropTables?.Contains(t.TableName, StringComparer.OrdinalIgnoreCase) == true));
+
if (seed != null)
result.Items.AddRange(await _recordCleaner.CleanAsync(seed, mode));
+ if (tables.Count > 0)
+ await _tableCleaner.DropAsync(tables, dropTables ?? [], connections, result.Items);
+
File.Delete(filePath);
result.Items.Add(new WizardDeleteLogItemDto
{
diff --git a/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs b/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs
index 36de83c6..925bbdfe 100644
--- a/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs
+++ b/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs
@@ -1,14 +1,18 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Languages.Entities;
+using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Extensions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
+using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement;
using Volo.Abp.TenantManagement;
@@ -46,6 +50,8 @@ public class WizardRecordCleaner(
IRepository repoBackgroundWorker,
IRepository repoIntranetWidget,
ITenantRepository tenantRepository,
+ IConfiguration configuration,
+ ICurrentTenant currentTenant,
ILogger logger) : ITransientDependency
{
/// Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.
@@ -131,9 +137,46 @@ public class WizardRecordCleaner(
{
await repoListForm.DeleteAsync(listForm, autoSave: true);
Add(report, WizardRecordTypeEnum.ListForm, listFormCode, WizardDeleteStatusEnum.Deleted);
+
+ if (mode == WizardDeleteMode.Delete)
+ DeleteSeedDataMirror(listForm.SeedFilePath, report);
}
}
+ ///
+ /// Ekranin veri aynasi (ListForm.SeedFilePath) ekransiz uygulanamaz; ekranla birlikte
+ /// gider. Duzenlemede ekran ayni yolla yeniden kurulacagi icin dokunulmaz.
+ ///
+ private void DeleteSeedDataMirror(string? seedFilePath, List report)
+ {
+ var relativePath = seedFilePath?.Replace('\\', '/').Trim('/');
+ if (string.IsNullOrWhiteSpace(relativePath) ||
+ relativePath.Contains("..", StringComparison.Ordinal) ||
+ !relativePath.StartsWith(SeedPathResolver.DataFolder + "/", StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ var path = SeedPathResolver.GetScopePath(configuration, currentTenant.Id, relativePath.Split('/'));
+ if (SeedPathResolver.IsSeedFolderPath(relativePath))
+ {
+ if (!Directory.Exists(path))
+ return;
+
+ Directory.Delete(path, recursive: true);
+ }
+ else
+ {
+ if (!File.Exists(path))
+ return;
+
+ File.Delete(path);
+ }
+
+ Add(report, WizardRecordTypeEnum.SeedFile, relativePath, WizardDeleteStatusEnum.Deleted);
+ logger.LogInformation("Wizard seed data mirror deleted: {Path}", path);
+ }
+
private static string FieldName(ListFormField field) => $"{field.ListFormCode}.{field.FieldName}";
private static string CriteriaName(ListFormWorkflow criteria) =>
diff --git a/api/src/Sozsoft.Platform.Application/ListForms/WizardTableCleaner.cs b/api/src/Sozsoft.Platform.Application/ListForms/WizardTableCleaner.cs
new file mode 100644
index 00000000..001adf16
--- /dev/null
+++ b/api/src/Sozsoft.Platform.Application/ListForms/WizardTableCleaner.cs
@@ -0,0 +1,373 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Sozsoft.Platform.Data.Seeds;
+using Sozsoft.Platform.DynamicData;
+using Sozsoft.Platform.Entities;
+using Sozsoft.Platform.Enums;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Repositories;
+using Volo.Abp.MultiTenancy;
+
+namespace Sozsoft.Platform.ListForms;
+
+///
+/// Wizard silinirken ekranin tablosunu ve tabloya bagli seed artefaktlarini (tablo scripti,
+/// CRUD endpoint'leri ve crud/{Tablo}.json) kaldirir.
+///
+/// Tablo wizard'in urettigi bir kayit degildir; tasarimci ya da MCP create_table ile ayrica
+/// olusturulur. Bu yuzden kendiliginden silinmez: once adaylari dondurur,
+/// kullanici onay diyalogunda secer, yalnizca secilenleri dusurur.
+///
+///
+/// Aday yalnizca Table tipli ekranin tablosudur ve iki kosulla secilebilir: tablonun
+/// {sql|postgres}/object/{Tablo}.sql altinda CREATE TABLE scripti vardir (platform
+/// tablolari bu klasorde durmaz; scriptsiz tablo elle ya da migration ile gelmistir) ve baska bir
+/// ekran, Custom Endpoint ya da custom component tabloyu kullanmaz.
+///
+///
+public class WizardTableCleaner(
+ IRepository repoListForm,
+ IRepository repoCustomEndpoint,
+ IRepository repoCustomComponent,
+ IRepository repoCrudEndpoint,
+ IDynamicDataManager dynamicDataManager,
+ IConfiguration configuration,
+ ICurrentTenant currentTenant,
+ ILogger logger) : ITransientDependency
+{
+ private const int MaxUsedBy = 20;
+
+ public async Task> AnalyzeAsync(WizardSeedFileDto seed)
+ {
+ var wizard = seed?.Wizard;
+ if (wizard == null || wizard.ComponentKind == WizardComponentKindEnum.Custom)
+ return [];
+
+ var listFormCode = wizard.ResolveListFormCode();
+ var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
+
+ // Ekran tasarimcida degismis olabilir; kayit varsa esas odur.
+ var dataSourceCode = listForm?.DataSourceCode ?? wizard.DataSourceCode;
+ var commandType = listForm?.SelectCommandType ?? wizard.SelectCommandType;
+ var selectCommand = listForm?.SelectCommand ?? wizard.SelectCommand;
+
+ if (commandType != SelectCommandTypeEnum.Table ||
+ string.IsNullOrWhiteSpace(selectCommand) ||
+ string.IsNullOrWhiteSpace(dataSourceCode))
+ {
+ return [];
+ }
+
+ var (schemaName, tableName) = ParseTableName(selectCommand);
+ var table = new WizardDeleteTableDto
+ {
+ DataSourceCode = dataSourceCode,
+ SchemaName = schemaName,
+ TableName = tableName,
+ Files = FindSeedFiles(tableName),
+ };
+
+ await ReadTableStateAsync(table);
+ table.SchemaName ??= "dbo";
+
+ var usedBy = await FindUsagesAsync(table, listFormCode);
+ if (usedBy.Count > 0)
+ {
+ table.Reason = WizardDeleteReasonEnum.InUse;
+ table.UsedBy = [.. usedBy
+ .DistinctBy(u => (u.RecordType, u.Name?.ToLowerInvariant()))
+ .Take(MaxUsedBy)];
+ }
+ else if (!HasCreateTableScript(tableName))
+ {
+ table.Reason = WizardDeleteReasonEnum.NotManaged;
+ }
+ else
+ {
+ table.CanDrop = true;
+ }
+
+ return [table];
+ }
+
+ ///
+ /// Secilen tablolari dusurur ve tabloya bagli seed artefaktlarini siler; secilmeyen ya da
+ /// secilemeyen adaylar rapora nedeniyle yazilir. Veritabani hatasi silmeyi durdurmaz: tablo
+ /// yerinde kalir, hata raporda gosterilir ve tablonun scripti de korunur.
+ ///
+ /// Kayit temizliginden sonra cagrilir ki temizlik hata verirse geri alinamaz bir dusurme
+ /// yapilmamis olsun. Temizlik veri kaynagi kaydini silmis olabilecegi icin baglantilar ondan
+ /// once ile alinir.
+ ///
+ ///
+ public async Task DropAsync(
+ List tables,
+ IReadOnlyCollection selected,
+ Dictionary connections,
+ List report)
+ {
+ foreach (var table in tables)
+ {
+ var name = QualifiedName(table);
+
+ if (!table.CanDrop)
+ {
+ report.Add(new WizardDeleteLogItemDto
+ {
+ RecordType = WizardRecordTypeEnum.Table,
+ Name = name,
+ Status = WizardDeleteStatusEnum.Kept,
+ Reason = table.Reason,
+ UsedBy = table.UsedBy,
+ });
+ continue;
+ }
+
+ if (!selected.Contains(table.TableName, StringComparer.OrdinalIgnoreCase))
+ {
+ report.Add(new WizardDeleteLogItemDto
+ {
+ RecordType = WizardRecordTypeEnum.Table,
+ Name = name,
+ Status = WizardDeleteStatusEnum.Kept,
+ Reason = WizardDeleteReasonEnum.NotSelected,
+ });
+ continue;
+ }
+
+ if (table.Exists && !await TryDropTableAsync(table, connections.GetValueOrDefault(table.DataSourceCode), report))
+ continue;
+
+ if (!table.Exists)
+ report.Add(new WizardDeleteLogItemDto { RecordType = WizardRecordTypeEnum.Table, Name = name, Status = WizardDeleteStatusEnum.NotFound });
+
+ await DeleteCrudEndpointsAsync(table.TableName, report);
+ DeleteSeedFiles(table.Files, report);
+ }
+ }
+
+ /// Veri kaynagi kayitlari temizlenmeden once her tablonun baglantisini cozer.
+ public async Task> ResolveConnectionsAsync(IEnumerable tables)
+ {
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var code in tables.Where(t => t.CanDrop && t.Exists).Select(t => t.DataSourceCode).Distinct(StringComparer.OrdinalIgnoreCase))
+ {
+ var (repo, connectionString, dbType) = await dynamicDataManager.GetAsync(false, code);
+ result[code] = new TableConnection(repo, connectionString, dbType);
+ }
+
+ return result;
+ }
+
+ // ── Analiz ─────────────────────────────────────────────────────────────────────
+
+ private async Task ReadTableStateAsync(WizardDeleteTableDto table)
+ {
+ try
+ {
+ var (repo, connectionString, dbType) = await dynamicDataManager.GetAsync(false, table.DataSourceCode);
+ table.SchemaName ??= dbType == DataSourceTypeEnum.Postgresql ? "public" : "dbo";
+
+ var parameters = new Dictionary
+ {
+ ["schemaName"] = table.SchemaName,
+ ["tableName"] = table.TableName,
+ };
+
+ var exists = await repo.ExecuteScalarAsync(
+ "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = @schemaName AND table_name = @tableName AND table_type = 'BASE TABLE'",
+ connectionString,
+ parameters);
+
+ table.Exists = exists > 0;
+ if (!table.Exists)
+ return;
+
+ // Tablo adi parametre olarak baglanamaz; yalnizca katalogda var oldugu dogrulanan ad
+ // tirnaklanarak kullanilir.
+ table.RowCount = await repo.ExecuteScalarAsync(
+ $"SELECT COUNT(*) FROM {QuoteIdentifier(table.SchemaName, table.TableName, dbType)}",
+ connectionString);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Wizard table state could not be read: {DataSource}.{Table}", table.DataSourceCode, table.TableName);
+ }
+ }
+
+ private async Task> FindUsagesAsync(WizardDeleteTableDto table, string ownListFormCode)
+ {
+ var tableName = table.TableName;
+ var pattern = TablePattern(tableName);
+ var usedBy = new List();
+
+ // Ekran baska bir ekranin alt formuysa silinmez (WizardRecordCleaner); tablosu da kalir.
+ var quotedCode = $"\"{ownListFormCode}\"";
+ usedBy.AddRange((await repoListForm.GetListAsync(a =>
+ a.ListFormCode != ownListFormCode && a.SubFormsJson != null && a.SubFormsJson.Contains(quotedCode)))
+ .Select(l => Ref(WizardRecordTypeEnum.ListForm, l.ListFormCode)));
+
+ // Veritabani Contains ile on suzer, kelime siniri bellekte kontrol edilir; aksi halde
+ // "Tags" tablosu "TagsGroup" kullanan ekrani kullanan sayardi.
+ var listForms = await repoListForm.GetListAsync(a =>
+ a.ListFormCode != ownListFormCode &&
+ a.DataSourceCode == table.DataSourceCode &&
+ ((a.SelectCommand != null && a.SelectCommand.Contains(tableName)) ||
+ (a.InsertCommand != null && a.InsertCommand.Contains(tableName)) ||
+ (a.UpdateCommand != null && a.UpdateCommand.Contains(tableName)) ||
+ (a.DeleteCommand != null && a.DeleteCommand.Contains(tableName))));
+ usedBy.AddRange(listForms
+ .Where(l => new[] { l.SelectCommand, l.InsertCommand, l.UpdateCommand, l.DeleteCommand }
+ .Any(c => c != null && pattern.IsMatch(c)))
+ .Select(l => Ref(WizardRecordTypeEnum.ListForm, l.ListFormCode)));
+
+ var endpoints = await repoCustomEndpoint.GetListAsync(a =>
+ a.DataSourceCode == table.DataSourceCode && a.Sql != null && a.Sql.Contains(tableName));
+ usedBy.AddRange(endpoints
+ .Where(e => pattern.IsMatch(e.Sql))
+ .Select(e => Ref(WizardRecordTypeEnum.CustomEndpoint, e.Name)));
+
+ // Bilesenin veri kaynaklari CRUD endpoint'lerinin entity adini tasir (tablo adi).
+ var quotedTable = $"\"{tableName}\"";
+ usedBy.AddRange((await repoCustomComponent.GetListAsync(a => a.DataSources != null && a.DataSources.Contains(quotedTable)))
+ .Select(c => Ref(WizardRecordTypeEnum.CustomComponent, c.Name)));
+
+ return usedBy;
+ }
+
+ private static Regex TablePattern(string tableName) =>
+ new($@"(? FindSeedFiles(string tableName)
+ {
+ var fileName = SeedPathResolver.GetRowFileName(tableName);
+ if (fileName == null)
+ return [];
+
+ var scriptName = Path.ChangeExtension(fileName, ".sql");
+ var candidates = new[]
+ {
+ $"{SeedPathResolver.SqlFolder}/{SeedPathResolver.ObjectFolder}/{scriptName}",
+ $"{SeedPathResolver.PostgresFolder}/{SeedPathResolver.ObjectFolder}/{scriptName}",
+ $"{SeedPathResolver.CrudFolder}/{fileName}",
+ };
+
+ return [.. candidates.Where(path => File.Exists(ScopeFile(path)))];
+ }
+
+ private bool HasCreateTableScript(string tableName)
+ {
+ var scriptName = Path.ChangeExtension(SeedPathResolver.GetRowFileName(tableName) ?? string.Empty, ".sql");
+
+ foreach (var provider in new[] { SeedPathResolver.SqlFolder, SeedPathResolver.PostgresFolder })
+ {
+ var path = ScopeFile($"{provider}/{SeedPathResolver.ObjectFolder}/{scriptName}");
+ if (File.Exists(path) && Regex.IsMatch(File.ReadAllText(path), @"\bCREATE\s+TABLE\b", RegexOptions.IgnoreCase))
+ return true;
+ }
+
+ return false;
+ }
+
+ // ── Dusurme ────────────────────────────────────────────────────────────────────
+
+ private async Task TryDropTableAsync(WizardDeleteTableDto table, TableConnection? connection, List report)
+ {
+ var name = QualifiedName(table);
+ try
+ {
+ if (connection == null)
+ throw new InvalidOperationException($"'{table.DataSourceCode}' veri kaynağı çözülemedi.");
+
+ await connection.Repository.ExecuteAsync(
+ $"DROP TABLE {QuoteIdentifier(table.SchemaName, table.TableName, connection.DataSourceType)}",
+ connection.ConnectionString);
+
+ report.Add(new WizardDeleteLogItemDto { RecordType = WizardRecordTypeEnum.Table, Name = name, Status = WizardDeleteStatusEnum.Deleted });
+ logger.LogInformation("Wizard table dropped: {DataSource}.{Table}", table.DataSourceCode, name);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ // Yabanci anahtar gibi bir engel tabloyu yerinde birakir; scripti de korunur ki tablo
+ // ile seed klasoru birbirinden kopmasin.
+ logger.LogWarning(ex, "Wizard table could not be dropped: {DataSource}.{Table}", table.DataSourceCode, name);
+ report.Add(new WizardDeleteLogItemDto
+ {
+ RecordType = WizardRecordTypeEnum.Table,
+ Name = name,
+ Status = WizardDeleteStatusEnum.Kept,
+ Reason = WizardDeleteReasonEnum.Failed,
+ Message = ex.Message,
+ });
+ return false;
+ }
+ }
+
+ ///
+ /// Tablonun CRUD endpoint'leri tabloyla birlikte anlamsizlasir; SQL Explorer'daki tablo
+ /// dusurme akisiyla ayni kural.
+ ///
+ private async Task DeleteCrudEndpointsAsync(string tableName, List report)
+ {
+ var endpoints = await repoCrudEndpoint.GetListAsync(a => a.EntityName == tableName);
+ if (endpoints.Count == 0)
+ return;
+
+ await repoCrudEndpoint.DeleteManyAsync(endpoints, autoSave: true);
+ foreach (var endpoint in endpoints)
+ {
+ report.Add(new WizardDeleteLogItemDto
+ {
+ RecordType = WizardRecordTypeEnum.CrudEndpoint,
+ Name = $"{endpoint.Method} {endpoint.Path}",
+ Status = WizardDeleteStatusEnum.Deleted,
+ });
+ }
+ }
+
+ private void DeleteSeedFiles(IEnumerable files, List report)
+ {
+ foreach (var file in files)
+ {
+ var path = ScopeFile(file);
+ if (!File.Exists(path))
+ continue;
+
+ File.Delete(path);
+ report.Add(new WizardDeleteLogItemDto { RecordType = WizardRecordTypeEnum.SeedFile, Name = file, Status = WizardDeleteStatusEnum.Deleted });
+ logger.LogInformation("Wizard table seed file deleted: {FilePath}", path);
+ }
+ }
+
+ // ── Yardimcilar ────────────────────────────────────────────────────────────────
+
+ private string ScopeFile(string relativePath) =>
+ SeedPathResolver.GetScopePath(configuration, currentTenant.Id, relativePath.Split('/'));
+
+ /// Sema yazilmamissa null doner; varsayilan sema saglayiciya gore 'ta atanir.
+ private static (string? SchemaName, string TableName) ParseTableName(string selectCommand)
+ {
+ var parts = selectCommand.Split('.');
+ var schemaName = parts.Length > 1 ? parts[^2].Trim().Trim('"', '[', ']') : null;
+ return (schemaName, parts[^1].Trim().Trim('"', '[', ']'));
+ }
+
+ private static string QuoteIdentifier(string schemaName, string tableName, DataSourceTypeEnum dbType) =>
+ dbType == DataSourceTypeEnum.Postgresql
+ ? $"\"{schemaName.Replace("\"", "\"\"")}\".\"{tableName.Replace("\"", "\"\"")}\""
+ : $"[{schemaName.Replace("]", "]]")}].[{tableName.Replace("]", "]]")}]";
+
+ private static string QualifiedName(WizardDeleteTableDto table) => $"{table.SchemaName}.{table.TableName}";
+
+ private static WizardRecordRefDto Ref(WizardRecordTypeEnum type, string name) => new() { RecordType = type, Name = name };
+}
+
+/// Tablonun dusurulecegi baglanti; veri kaynagi kaydi silinmeden once cozulur.
+public sealed record TableConnection(IDynamicDataRepository Repository, string ConnectionString, DataSourceTypeEnum DataSourceType);
diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json
index 8c0b9ad8..3c181b4c 100644
--- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json
+++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json
@@ -16632,6 +16632,90 @@
"en": "Tenant",
"tr": "Kiracı"
},
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardRecordType.Table",
+ "en": "Table",
+ "tr": "Tablo"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardRecordType.CrudEndpoint",
+ "en": "CRUD endpoint",
+ "tr": "CRUD endpoint"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardRecordType.CustomComponent",
+ "en": "Custom component",
+ "tr": "Custom component"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteReason.NotSelected",
+ "en": "Not selected for deletion",
+ "tr": "Silinmek üzere seçilmedi"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteReason.NotManaged",
+ "en": "Not created with the table designer or create_table; it cannot be dropped from here",
+ "tr": "Tablo tasarımcısı ya da create_table ile oluşturulmamış; buradan silinemez"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteReason.Failed",
+ "en": "Operation failed",
+ "tr": "İşlem başarısız"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteTables",
+ "en": "Screen table",
+ "tr": "Ekranın tablosu"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteTablesHint",
+ "en": "The table is kept unless you select it. A selected table is dropped together with its CRUD endpoints and seed files (sql/object, crud).",
+ "tr": "Seçmezseniz tablo veritabanında kalır. Seçilen tablo CRUD endpoint'leri ve seed dosyalarıyla (sql/object, crud) birlikte silinir."
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteTablesLoading",
+ "en": "Checking the screen table...",
+ "tr": "Ekranın tablosu kontrol ediliyor..."
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteTableRows",
+ "en": "{0} rows",
+ "tr": "{0} kayıt"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteTableMissing",
+ "en": "not in the database",
+ "tr": "veritabanında yok"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteTableWarning",
+ "en": "The table and all of its data will be permanently deleted. This cannot be undone. Files to be deleted:",
+ "tr": "Tablo ve içindeki tüm veriler kalıcı olarak silinecek. Bu işlem geri alınamaz. Silinecek dosyalar:"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteTableNoPermission",
+ "en": "Dropping a table requires the SQL Query Manager permission.",
+ "tr": "Tablo silmek için SQL Query Manager yetkisi gerekir."
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardDeleteWithTables",
+ "en": "Delete with table",
+ "tr": "Tabloyla birlikte sil"
+ },
{
"resourceName": "Platform",
"key": "App.ListForm.WizardFileExport",
@@ -28818,6 +28902,18 @@
"en": "The note to append cannot be empty.",
"tr": "Eklenecek not boş olamaz."
},
+ {
+ "resourceName": "Platform",
+ "key": "App.Mcp.Error.DevRequestSubTasksRequired",
+ "en": "At least one sub task title is required.",
+ "tr": "En az bir alt görev başlığı gerekir."
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.Mcp.Error.DevRequestSubTasksInvalid",
+ "en": "The Sub Tasks of development request \"{0}\" could not be read; fix it on the request screen first.",
+ "tr": "\"{0}\" geliştirme talebinin alt görevleri okunamadı; önce talep ekranından düzeltin."
+ },
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VisualDesignerComponent",
diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardRecordTypeEnum.cs b/api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardRecordTypeEnum.cs
index be6a00cf..5def9f34 100644
--- a/api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardRecordTypeEnum.cs
+++ b/api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardRecordTypeEnum.cs
@@ -19,10 +19,17 @@ public enum WizardRecordTypeEnum
DataSource = 9,
LanguageKey = 10,
+ /// Ekranin tablosu; yalnizca silme onayinda secildiyse dusurulur.
+ Table = 11,
+
+ /// Dusurulen tablonun CRUD endpoint'leri.
+ CrudEndpoint = 12,
+
CustomEndpoint = 20,
BackgroundWorker = 21,
IntranetWidget = 22,
Tenant = 23,
+ CustomComponent = 24,
}
/// Silme raporunda bir kaydin akibeti.
@@ -47,4 +54,16 @@ public enum WizardDeleteReasonEnum
/// Duzenleme yolunda paylasilan kayit korunur; deploy onu yeniden kullanir.
Redeploy = 2,
+
+ /// Tablo silme onayinda secilmedi; tablo ve ona bagli seed dosyalari yerinde kalir.
+ NotSelected = 3,
+
+ ///
+ /// Tablo tasarimcida ya da MCP create_table ile olusturulmamis (seed scripti yok);
+ /// platform tablosu olabilecegi icin silme onayinda secilemez.
+ ///
+ NotManaged = 4,
+
+ /// Islem veritabaninda hata verdi; ayrinti raporun mesajindadir.
+ Failed = 5,
}
diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs
index 52e5341b..e6e364dd 100644
--- a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs
+++ b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs
@@ -52,6 +52,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling;
using Volo.Abp.AspNetCore.Serilog;
+using Volo.Abp.AspNetCore.Uow;
using Volo.Abp.Autofac;
using Volo.Abp.BackgroundWorkers.Hangfire;
using Volo.Abp.BlobStoring;
@@ -86,6 +87,8 @@ namespace Sozsoft.Platform;
)]
public class PlatformHttpApiHostModule : AbpModule
{
+ private const string McpEndpointPath = "/mcp";
+
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
@@ -151,6 +154,7 @@ public class PlatformHttpApiHostModule : AbpModule
context.Services.AddHostedService();
ConfigureBlobStoring(configuration);
ConfigureAuditing();
+ ConfigureUnitOfWork();
context.Services.AddResponseCompression(options =>
{
@@ -413,6 +417,20 @@ public class PlatformHttpApiHostModule : AbpModule
});
}
+ ///
+ /// MCP cevabi SSE ile akar: istek duzeyindeki unit of work arac sonucu istemciye gittikten sonra
+ /// commit edilir ve istemci baglantiyi kapattiginda (RequestAborted) geri alinir. Arac basarili
+ /// gorunur ama EF kayitlari kaybolur; DDL ve seed dosyasi ise kalir. Uc bu yuzden istek
+ /// UoW'sinin disindadir; her servis cagrisi kendi UoW'sini sonuc donmeden tamamlar.
+ ///
+ private void ConfigureUnitOfWork()
+ {
+ Configure(options =>
+ {
+ options.IgnoredUrls.Add(McpEndpointPath);
+ });
+ }
+
private void ConfigureDynamicServices(ServiceConfigurationContext context)
{
// Dynamic AppService Background Service
@@ -567,14 +585,15 @@ public class PlatformHttpApiHostModule : AbpModule
endpoints.MapHub("/messengerhub");
endpoints.MapHub("/notificationhub");
- // MCP (Model Context Protocol) ucu. Kimlik dogrulama, kiraci cozumu ve unit of work
- // yukaridaki ardisik duzenden gelir. Uc yalnizca kimlik ister; Invoke her MCP
- // servisinin sinif duzeyindeki [Authorize] attribute'unda aranir. Uca konsaydi yetkisiz
+ // MCP (Model Context Protocol) ucu. Kimlik dogrulama ve kiraci cozumu yukaridaki
+ // ardisik duzenden gelir; istek UoW'si bu uca uygulanmaz (ConfigureUnitOfWork). Uc
+ // yalnizca kimlik ister; Invoke her MCP servisinin sinif duzeyindeki [Authorize]
+ // attribute'unda aranir. Uca konsaydi yetkisiz
// kullanici nedeni belirtilmeyen bir 403 alirdi; servis duzeyinde reddedildiginde
// istemci eksik yetkiyi adiyla gorur (McpToolErrorTranslator). Sema acikca bearer'a
// baglanir: varsayilan cerez semasi token'siz istegi giris sayfasina 302 ile
// yonlendirir, MCP istemcisi 401 bekler.
- endpoints.MapMcp("/mcp").RequireAuthorization(new AuthorizeAttribute
+ endpoints.MapMcp(McpEndpointPath).RequireAuthorization(new AuthorizeAttribute
{
AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme
});
diff --git a/claude.md b/claude.md
index e22df2e6..f1d39e25 100644
--- a/claude.md
+++ b/claude.md
@@ -210,6 +210,10 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
yazma ekranın C/U/D sözleşmesinden geçtiği için kiracıya açıktır; ekran yetkisinden geçmeden
yazabilen araç (procedure, Custom Endpoint) ve geliştirme araçları (`DevelopTools`) `TenantTools`'a
eklenmez. Geliştirme işlemi yine Wizard / Custom Components AppService'inden geçer.
+ **`/mcp` istek düzeyindeki unit of work'ün dışındadır** (`AbpAspNetCoreUnitOfWorkOptions.IgnoredUrls`):
+ cevap SSE ile aktığı için istek UoW'si sonuç istemciye gittikten sonra commit edilir ve bağlantı
+ kapanınca geri alınır — araç başarılı görünür, EF kayıtları kaybolur. Her MCP servis çağrısı
+ kendi UoW'sini sonuç dönmeden tamamlar; bu istisna kaldırılmaz.
Kapı iki yerdedir: `AddListToolsFilter` listeyi süzer, `McpAppService.EnsureToolAllowedAsync`
çağrıyı reddeder — süzme tek başına yetki sınırı değildir, ikisi birlikte yazılır. `Invoke`
uçta değil her MCP servisinin sınıf düzeyindeki `[Authorize]`'dadır; yeni bir MCP AppService'i de
@@ -235,7 +239,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
kalır, hiçbir yere yüklenmez. `start_dev_request` / `get_dev_request` onu metinden ayıklar
(`McpDevRequestImages`) ve MCP resim içeriği (`ImageContentBlock`) olarak döndürür; metinde
`[image-N]` kalır. Açıklama bu yüzden `update_row` ile yeniden yazılmaz, not
- `append_dev_request_note` ile eklenir.
+ `append_dev_request_note` ile eklenir. `SubTasks` de `add_dev_request_subtasks` ile yazılır:
+ atanan kişi bağlanan kullanıcı, bitiş tarihi bugün — sunucu doldurur, modele bırakılmaz.
- **MCP şema araçları** → tablo/sütun bilgisi canlı katalogdan değil `Sas_H_DbTableColumn`
sözlüğünden okunur (`IMcpSchemaDictionary`). Sözlüğün alan adları
`dbo.Sas_H_ListFormFieldsDictionaryJson` prosedürünün ürettiği JSON ile birebir aynıdır;
diff --git a/ui/src/constants/permission.constant.ts b/ui/src/constants/permission.constant.ts
index f3fe4bc6..f2ebe5ef 100644
--- a/ui/src/constants/permission.constant.ts
+++ b/ui/src/constants/permission.constant.ts
@@ -17,6 +17,8 @@ export const WIZARD_PERMISSION = {
DELETE: 'App.Listforms.Wizard.Delete',
EXPORT: 'App.Listforms.Wizard.Export',
IMPORT: 'App.Listforms.Wizard.Import',
+ /** Wizard silinirken ekranın tablosunu düşürmek SQL Query Manager yetkisi ister. */
+ DROP_TABLE: 'App.SqlQueryManager',
} as const
/**
diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts
index a0deb9ca..fd4ef0ef 100644
--- a/ui/src/proxy/admin/wizard/models.ts
+++ b/ui/src/proxy/admin/wizard/models.ts
@@ -158,10 +158,13 @@ export enum WizardRecordTypeEnum {
Module = 8,
DataSource = 9,
LanguageKey = 10,
+ Table = 11,
+ CrudEndpoint = 12,
CustomEndpoint = 20,
BackgroundWorker = 21,
IntranetWidget = 22,
Tenant = 23,
+ CustomComponent = 24,
}
export enum WizardDeleteStatusEnum {
@@ -174,6 +177,9 @@ export enum WizardDeleteReasonEnum {
None = 0,
InUse = 1,
Redeploy = 2,
+ NotSelected = 3,
+ NotManaged = 4,
+ Failed = 5,
}
export interface WizardRecordRefDto {
@@ -187,6 +193,8 @@ export interface WizardDeleteLogItemDto {
status: WizardDeleteStatusEnum
reason: WizardDeleteReasonEnum
usedBy: WizardRecordRefDto[]
+ /** `Failed` durumunda veritabanının hata mesajı. */
+ message?: string
}
/** Silinen, korunan ve bulunamayan her kayıt tek tek listelenir. */
@@ -196,6 +204,31 @@ export interface WizardDeleteResultDto {
items: WizardDeleteLogItemDto[]
}
+/** Silme onayında sorulan, ekranın tablosu; yalnızca `canDrop` olan seçilebilir. */
+export interface WizardDeleteTableDto {
+ dataSourceCode: string
+ schemaName: string
+ tableName: string
+ exists: boolean
+ rowCount?: number
+ canDrop: boolean
+ reason: WizardDeleteReasonEnum
+ usedBy: WizardRecordRefDto[]
+ /** Tabloyla birlikte silinecek seed dosyaları (kapsam klasörüne göre). */
+ files: string[]
+}
+
+export interface WizardDeleteAnalysisDto {
+ fileName: string
+ wizardName: string
+ tables: WizardDeleteTableDto[]
+}
+
+export interface WizardDeleteInput {
+ fileName: string
+ dropTables: string[]
+}
+
export interface WizardInsertedRecordsDto {
languageKeys: string[]
permissionGroupNames: string[]
diff --git a/ui/src/services/wizard.service.ts b/ui/src/services/wizard.service.ts
index 8428bc3b..7b3a61ed 100644
--- a/ui/src/services/wizard.service.ts
+++ b/ui/src/services/wizard.service.ts
@@ -1,6 +1,8 @@
import {
ListFormWizardDto,
WizardExportAnalysisDto,
+ WizardDeleteAnalysisDto,
+ WizardDeleteInput,
WizardDeleteResultDto,
WizardFileInfoDto,
WizardImportAnalysisDto,
@@ -38,13 +40,21 @@ export const getWizardFile = (fileName: string) =>
params: { fileName },
})
-export const deleteWizardFile = (fileName: string) =>
- apiService.fetchData({
- method: 'DELETE',
- url: `/api/app/list-form-wizard/file`,
+/** Silme onayında sorulacak tabloları getirir; hiçbir şeye dokunmaz. */
+export const getWizardDeleteAnalysis = (fileName: string) =>
+ apiService.fetchData({
+ method: 'GET',
+ url: `/api/app/list-form-wizard/delete-analysis`,
params: { fileName },
})
+export const deleteWizardFile = (input: WizardDeleteInput) =>
+ apiService.fetchData({
+ method: 'POST',
+ url: `/api/app/list-form-wizard/delete-file`,
+ data: input,
+ })
+
/**
* Indirmeden once arsivin tam icerigini ve cozulemeyen bagimliliklarini getirir; export ile
* ayni toplamayi kullanir, dolayisiyla onizlemedeki liste indirilen zip ile birebir aynidir.
diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx b/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx
index 27dd4698..cf115e9e 100644
--- a/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx
+++ b/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx
@@ -72,7 +72,7 @@ function JsonRowOpDialogEditForm({
{translate('::App.Platform.DeleteConfirmation')}
-
+
@@ -149,7 +149,7 @@ function JsonRowOpDialogEditForm({
-
+
diff --git a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx
index b45e1853..c20f7d52 100644
--- a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx
+++ b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import classNames from 'classnames'
-import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
+import { Button, Checkbox, Dialog, Input, Notification, toast } from '@/components/ui'
import Container from '@/components/shared/Container'
import Loading from '@/components/shared/Loading'
import PageTitle from '@/components/shared/PageTitle'
@@ -32,6 +32,7 @@ import {
deleteWizardFile,
downloadBlob,
exportWizardFile,
+ getWizardDeleteAnalysis,
getWizardExportAnalysis,
getWizardFiles,
} from '@/services/wizard.service'
@@ -45,6 +46,7 @@ import {
WizardDeleteReasonEnum,
WizardDeleteResultDto,
WizardDeleteStatusEnum,
+ WizardDeleteTableDto,
WizardExportAnalysisDto,
WizardFileInfoDto,
WizardRecordTypeEnum,
@@ -61,6 +63,10 @@ const VIEW_STATE_CODE = 'developerkit-wizardmanager'
interface ConfirmState {
fileName: string
wizardName: string
+ /** Ekranın tablosu; analiz gelene kadar null. */
+ tables: WizardDeleteTableDto[] | null
+ /** Onayda seçilen, düşürülecek tablolar. */
+ dropTables: string[]
}
const DELETE_STATUS_ORDER = [
@@ -186,6 +192,7 @@ const WizardFileManager = () => {
const canDelete = checkPermission(WIZARD_PERMISSION.DELETE)
const canExport = checkPermission(WIZARD_PERMISSION.EXPORT)
const canImport = checkPermission(WIZARD_PERMISSION.IMPORT)
+ const canDropTable = checkPermission(WIZARD_PERMISSION.DROP_TABLE)
const menuPathText = useCallback(
(f: WizardFileInfoDto) => (f.menuPath ?? []).map((key) => translate('::' + key)).join(' › '),
@@ -312,12 +319,45 @@ const WizardFileManager = () => {
if (file) setImportFile(file)
}
+ /**
+ * Onay açılırken ekranın tablosu sorulur. Tablo wizard'ın ürettiği bir kayıt olmadığı için
+ * kendiliğinden silinmez; kullanıcı burada seçerse düşürülür. Analiz alınamazsa silme yine
+ * yapılabilir, yalnızca tablo sorulmaz.
+ */
+ const openDeleteConfirm = async (f: WizardFileInfoDto) => {
+ const base = { fileName: f.fileName, wizardName: f.wizardName || f.fileName, dropTables: [] }
+ setConfirm({ ...base, tables: null })
+ try {
+ const res = await getWizardDeleteAnalysis(f.fileName)
+ setConfirm((prev) =>
+ prev?.fileName === f.fileName ? { ...prev, tables: res.data.tables ?? [] } : prev,
+ )
+ } catch {
+ setConfirm((prev) => (prev?.fileName === f.fileName ? { ...prev, tables: [] } : prev))
+ }
+ }
+
+ const toggleDropTable = (tableName: string, checked: boolean) =>
+ setConfirm((prev) =>
+ prev
+ ? {
+ ...prev,
+ dropTables: checked
+ ? [...prev.dropTables, tableName]
+ : prev.dropTables.filter((t) => t !== tableName),
+ }
+ : prev,
+ )
+
const handleDeleteConfirm = async () => {
if (!confirm) return
setDeletingFile(confirm.fileName)
setConfirm(null)
try {
- const res = await deleteWizardFile(confirm.fileName)
+ const res = await deleteWizardFile({
+ fileName: confirm.fileName,
+ dropTables: confirm.dropTables,
+ })
setDeleteReport(res.data)
toast.push(
@@ -359,6 +399,58 @@ const WizardFileManager = () => {
.filter((x): x is WizardFileInfoDto => !!x)
.map((x) => x.wizardName || x.fileName)
+ /** Seçilemeyen tablo nedeniyle gösterilir; seçilen tablo için geri alınamaz uyarısı çıkar. */
+ const renderDropTable = (table: WizardDeleteTableDto, selected: string[]) => {
+ const checked = selected.includes(table.tableName)
+ return (
+
+
toggleDropTable(table.tableName, value)}
+ >
+
+ {table.schemaName}.{table.tableName}
+
+
+ {table.dataSourceCode} ·{' '}
+ {table.exists
+ ? translate('::App.ListForm.WizardDeleteTableRows', { 0: table.rowCount ?? '?' })
+ : translate('::App.ListForm.WizardDeleteTableMissing')}
+
+
+ {!table.canDrop && (
+
+ {translate(`::App.ListForm.WizardDeleteReason.${WizardDeleteReasonEnum[table.reason]}`)}
+ {table.usedBy.map((ref) => (
+
+ {recordTypeText(ref.recordType)}: {ref.name}
+
+ ))}
+
+ )}
+ {table.canDrop && !canDropTable && (
+
+ {translate('::App.ListForm.WizardDeleteTableNoPermission')}
+
+ )}
+ {checked && (
+
+ {translate('::App.ListForm.WizardDeleteTableWarning')}
+ {table.files.map((file) => (
+
+ {file}
+
+ ))}
+
+ )}
+
+ )
+ }
+
const renderActions = (f: WizardFileInfoDto, className: string) => (
{!f.hasInsertedRecords && (
@@ -425,9 +517,7 @@ const WizardFileManager = () => {
loading={deletingFile === f.fileName}
icon={}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
- onClick={() =>
- setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
- }
+ onClick={() => openDeleteConfirm(f)}
/>
)}
@@ -835,13 +925,40 @@ const WizardFileManager = () => {
+ {confirm?.tables === null && (
+
+ {translate('::App.ListForm.WizardDeleteTablesLoading')}
+
+ )}
+ {!!confirm?.tables?.length && (
+
+
+ {translate('::App.ListForm.WizardDeleteTables')}
+
+
+ {translate('::App.ListForm.WizardDeleteTablesHint')}
+
+ {confirm.tables.map((table) => renderDropTable(table, confirm.dropTables))}
+
+ )}
-
+
-
@@ -916,6 +1033,11 @@ const WizardFileManager = () => {
)}
)}
+ {item.message && (
+
+ {item.message}
+
+ )}
{item.usedBy.map((ref) => (
{recordTypeText(ref.recordType)}:{' '}
@@ -929,7 +1051,7 @@ const WizardFileManager = () => {
-
+
setDeleteReport(null)}>
{translate('::App.Platform.Close')}
@@ -977,7 +1099,7 @@ const WizardFileManager = () => {
-
+
setExportPreview(null)}>
{translate('::App.Platform.Cancel')}
diff --git a/ui/src/views/admin/listForm/wizard/WizardStepSubForms.tsx b/ui/src/views/admin/listForm/wizard/WizardStepSubForms.tsx
index e2e24da2..fdd8e664 100644
--- a/ui/src/views/admin/listForm/wizard/WizardStepSubForms.tsx
+++ b/ui/src/views/admin/listForm/wizard/WizardStepSubForms.tsx
@@ -445,7 +445,7 @@ function WizardStepSubForms({
/>
-
+
{translate('::App.Platform.Delete')}
{translate('::App.Platform.DeleteConfirmation')}
-
+
setDeleteIndex(null)}>
{translate('::App.Platform.Cancel')}
diff --git a/ui/src/views/admin/listForm/wizard/WizardStepWidgets.tsx b/ui/src/views/admin/listForm/wizard/WizardStepWidgets.tsx
index d3867d2c..2138354c 100644
--- a/ui/src/views/admin/listForm/wizard/WizardStepWidgets.tsx
+++ b/ui/src/views/admin/listForm/wizard/WizardStepWidgets.tsx
@@ -320,7 +320,7 @@ function WizardStepWidgets({ widgets, translate, onChange, onBack, onNext }: Pro
-
+
setDialogIndex(null)}>
{translate('::App.Platform.Cancel')}
@@ -343,7 +343,7 @@ function WizardStepWidgets({ widgets, translate, onChange, onBack, onNext }: Pro
{translate('::App.Platform.Delete')}
{translate('::App.Platform.DeleteConfirmation')}
-
+
setDeleteIndex(null)}>
{translate('::App.Platform.Cancel')}