WizardFileManager güncellemesi

This commit is contained in:
Sedat ÖZTÜRK 2026-09-30 23:52:33 +03:00
parent 6081131f95
commit 0bc44e726c
24 changed files with 757 additions and 74 deletions

View file

@ -103,8 +103,8 @@ ve çoğu isteğin cevabı aslında oradadır:
| `DataSourceCode` | `"Default"` | Hangi bağlantıdan okunacağı (`Sas_H_DataSource`) | | `DataSourceCode` | `"Default"` | Hangi bağlantıdan okunacağı (`Sas_H_DataSource`) |
| `SelectCommandType` | `1` Table | `1` Table · `2` View · `3` TableValuedFunction · `4` Query · `5` StoredProcedure | | `SelectCommandType` | `1` Table | `1` Table · `2` View · `3` TableValuedFunction · `4` Query · `5` StoredProcedure |
| `SelectCommand` | — | Tablo/view adı, fonksiyon çağrısı ya da sorgu metni | | `SelectCommand` | — | Tablo/view adı, fonksiyon çağrısı ya da sorgu metni |
| `TableName` | `SelectCommand` | Üretilen `SELECT`'te kolonların ve join'lerin önüne yazılan **alias**. Boşsa `SelectCommand` kullanılır; `SelectCommand` bir sorgu metni ya da fonksiyon çağrısıysa buraya kısa bir ad yaz | | `TableName` | `SelectCommand` | Üretilen `SELECT`'te kolonların ve join'lerin önüne yazılan **alias**. Boşsa `SelectCommand` kullanılır. `4` Query'de **zorunludur**: sorgu `FROM (…) AS "{TableName}"` biçiminde türetilmiş tablo olarak sarılır ve boş bırakılırsa sorgu metni alias olur (SQL hatası). Wizard/MCP `deploy_wizard` Query ekranına `"Q"` (`WizardConsts.QuerySourceAlias`) yazar; fonksiyon çağrısında da kısa bir ad yaz |
| `KeyFieldName` · `KeyFieldDbSourceType` | — · `16` String | Anahtar kolon ve `System.Data.DbType` karşılığı | | `KeyFieldName` · `KeyFieldDbSourceType` | — · `16` String | Anahtar kolon ve `System.Data.DbType` karşılığı. `4` Query'de ikisi de **zorunludur** (kolonlar katalogdan okunamaz); wizard/`deploy_wizard` anahtarsız Query ekranını reddeder, tip boşsa (`0`) seçilen alanlardaki karşılığından tamamlar. Ana tablo (en dıştaki `FROM`) `IsDeleted` taşıyorsa `DefaultFilter` yazılır ve sorgu `IsDeleted`'i seçmek zorundadır |
| `SelectFieldsDefaultValueJson` | — | **Okuma** sorgusuna parametre olarak katılan değerler (`FieldsDefaultValueDto[]`) — `@USERID`, `@TENANTID` gibi | | `SelectFieldsDefaultValueJson` | — | **Okuma** sorgusuna parametre olarak katılan değerler (`FieldsDefaultValueDto[]`) — `@USERID`, `@TENANTID` gibi |
| `DefaultFilter` | — | Her sorgunun sonuna `AND` ile eklenen sabit `WHERE` koşulu. `DefaultValueHelper` üzerinden geçer, yani `@USERID`/`@TENANTID`/`@NOW` gibi token'lar çözülür. Soft delete'li tabloda (`IsDeletedField: true`) wizard buraya `"IsDeleted" = 'false'` yazar | | `DefaultFilter` | — | Her sorgunun sonuna `AND` ile eklenen sabit `WHERE` koşulu. `DefaultValueHelper` üzerinden geçer, yani `@USERID`/`@TENANTID`/`@NOW` gibi token'lar çözülür. Soft delete'li tabloda (`IsDeletedField: true`) wizard buraya `"IsDeleted" = 'false'` yazar |
| `SortMode` | — | **Metin**: `"single"` \| `"multiple"` \| `"none"` (`PlatformConsts.GridOptions.SortMode*`; wizard `single` yazar) | | `SortMode` | — | **Metin**: `"single"` \| `"multiple"` \| `"none"` (`PlatformConsts.GridOptions.SortMode*`; wizard `single` yazar) |
@ -375,7 +375,7 @@ Wizard varsayılan olarak `DefaultFieldPermissionJson(permissionName)` üretir.
| Anahtar | Anlam | | Anahtar | Anlam |
| --- | --- | | --- | --- |
| `TabType` | Sekmenin görünümü — `ListFormTabTypeEnum`: `List` \| `Card` \| `Tree` \| `Gantt` \| `Scheduler` \| `Todo` \| `Form` \| `Chart` \| `Pivot` | | `TabType` | Sekmenin görünümü — `ListFormTabTypeEnum`: `List` \| `Card` \| `Tree` \| `Gantt` \| `Scheduler` \| `Todo` \| `Form` \| `Chart` \| `Pivot`. `Form` ana kayda bağlı **tek** kaydı gösterir: kayıt yokken "Yeni" düğmesi (hedef ekranda `AllowAdding` açıksa), kayıt varken yalnızca düzenleme; ikinci kayıt açılmaz |
| `TabTitle` | Dil anahtarı | | `TabTitle` | Dil anahtarı |
| `Code` | Hedef `ListFormCode` | | `Code` | Hedef `ListFormCode` |
| `IsRefresh` | Ana kayıt değişince yenilensin | | `IsRefresh` | Ana kayıt değişince yenilensin |
@ -432,6 +432,10 @@ Sorgu ekranın bağlantısında (`IsTenant` + `DataSourceCode` ile çözülen, `
satır ekleme/güncelleme/silme kartları yeniden çeker (`useListFormWidgets`). Yalnızca widget satır ekleme/güncelleme/silme kartları yeniden çeker (`useListFormWidgets`). Yalnızca widget
listesi değişir, GridDto değiştirilmez — görünüm durumu (sayfa, seçim) korunur. Kart sorgusu listesi değişir, GridDto değiştirilmez — görünüm durumu (sayfa, seçim) korunur. Kart sorgusu
ekranın filtresine bağlı olmadığı için filtre değişikliği kartları yenilemez. ekranın filtresine bağlı olmadığı için filtre değişikliği kartları yenilemez.
- Kart sorgusu alt form sekmesinde `Relation` ile süzülmez; bu yüzden alt form olarak üretilen
ekrana widget yazılmaz (menüsüz ya da menülü). Menüsüz (`CreateMenu: false`) wizard'ın `Widgets`'ı
deploy'da ayrıca düşürülür; ekran alt form olarak (`SubForms` sekmesi, `isSubForm`) açıldığında
şerit çizilmez ve yenilenmez.
### 5.4 `LayoutJson` (`LayoutDto`) ### 5.4 `LayoutJson` (`LayoutDto`)

View file

@ -259,7 +259,7 @@ bildirilir:
| Parametreli SQL kullanımı, yetki kontrolü, tenant izolasyonu | Tartışmaya açık değil | | Parametreli SQL kullanımı, yetki kontrolü, tenant izolasyonu | Tartışmaya açık değil |
| Depoda okunabilen tablo/kolon/menü/yetki adları | Önce ara, bulduğunu **doğrulat**, sıfırdan sorma | | Depoda okunabilen tablo/kolon/menü/yetki adları | Önce ara, bulduğunu **doğrulat**, sıfırdan sorma |
| Var olan bir tanım tablosunun (ülke, para birimi, departman, ünvan, birim) yeniden üretilip üretilmeyeceği | Üretilmez; mevcut tabloya lookup bağlanır (reference §12.6) | | Var olan bir tanım tablosunun (ülke, para birimi, departman, ünvan, birim) yeniden üretilip üretilmeyeceği | Üretilmez; mevcut tabloya lookup bağlanır (reference §12.6) |
| Liste ekranının üstündeki KPI kartları (`Widgets`) | List ekranı en az 3 kartla gelir, kartlar tablonun durum/bayrak/tarih/tutar kolonlarından seçilir (§4.6.1) | | Liste ekranının üstündeki KPI kartları (`Widgets`) | List ekranı en az 3 kartla gelir, kartlar tablonun durum/bayrak/tarih/tutar kolonlarından seçilir; alt form olarak üretilen ekrana widget yazılmaz (§4.6.1) |
Buna karşılık **her zaman sorulacak** üç şey vardır, çünkü yanlış varsayım geri dönülmez sonuç Buna karşılık **her zaman sorulacak** üç şey vardır, çünkü yanlış varsayım geri dönülmez sonuç
üretir: **hangi tabloya yazılacağı**, **kimin onaylayacağı** (onay süreci varsa) ve üretir: **hangi tabloya yazılacağı**, **kimin onaylayacağı** (onay süreci varsa) ve
@ -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. 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 | | `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 — alt form olarak üretilen ekran hariç (`Widgets: []`) — 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 |
@ -637,6 +637,11 @@ değildir) ve içindeki satırları taşır; satırlar demo veridir (§0.10.1).
Bu yüzden her `create_table` öncesi kolon listesi ekranın editörleriyle karşılaştırılır: editörü Bu yüzden her `create_table` öncesi kolon listesi ekranın editörleriyle karşılaştırılır: editörü
`dxImageUpload` olan kolon `nvarchar(MAX)` açılır — tablo kurulduktan sonra kolon değiştirilemez. `dxImageUpload` olan kolon `nvarchar(MAX)` açılır — tablo kurulduktan sonra kolon değiştirilemez.
Kural analizi talebin istediğinden uzaklaştırmaz; yalnızca platformun kurallarına uydurur. Kural analizi talebin istediğinden uzaklaştırmaz; yalnızca platformun kurallarına uydurur.
- **Alt form ağacı analizdeki sırayla deploy edilir:** en derindeki alt form önce, onu taşıyan
alt form sonra, ana ekran en son (§0.12 madde 6); ana ekranın `SubForms`'u deploy anında var olan
ekranlara işaret eder ve alt form yetkileri onun altına bu sırada taşınır. Alt form olarak üretilen
her ekran (menüsüz ya da menülü) boş `Widgets` ile, menüsüz olan `CreateMenu: false` ile, `Form`
sekmesinin hedefi `AllowAdding` + `AllowUpdating` açık gider (§4.6).
- **Üretim tek seferde biter:** `In Progress` → `Testing` tek geçiştir; talep durdurulmaz, - **Üretim tek seferde biter:** `In Progress` → `Testing` tek geçiştir; talep durdurulmaz,
`Analysis`'e geri alınmaz, kısmi ekran üretilmez. Üretilebilirlik analizde doğrulanmıştır `Analysis`'e geri alınmaz, kısmi ekran üretilmez. Üretilebilirlik analizde doğrulanmıştır
(§0.12). Döngü DB Migrate'i beklemez. (§0.12). Döngü DB Migrate'i beklemez.
@ -675,11 +680,11 @@ Kısa ve karar odaklı yaz; talebi tekrar anlatma.
"gerekirse", "ileride" bırakılmaz. "gerekirse", "ileride" bırakılmaz.
- Talebin açıkça yazdığı şey (menü yolu, alan, kapsam) **girdidir**: değiştirilmez, daraltılmaz, - Talebin açıkça yazdığı şey (menü yolu, alan, kapsam) **girdidir**: değiştirilmez, daraltılmaz,
alternatifi sunulmaz. alternatifi sunulmaz.
- Talepte olmayan bilgi için analiz karar verir (**Kararlar**, madde 8). Tabloya yazma hedefi, - Talepte olmayan bilgi için analiz karar verir (**Kararlar**, madde 9). Tabloya yazma hedefi,
onaycı ve hesap formülü de boş bırakılmaz: somut karar **(onay)** etiketiyle yazılır; talebi onaycı ve hesap formülü de boş bırakılmaz: somut karar **(onay)** etiketiyle yazılır; talebi
`Ready to Start`'a alan kullanıcı onları onaylamış olur. `Ready to Start`'a alan kullanıcı onları onaylamış olur.
- Planın her adımının mevcut araçlarla (`create_table`, `deploy_wizard`, `save_component` ve - Planın her adımının mevcut araçlarla (`create_table`, `deploy_wizard`, `save_component` ve
sözleşmeleri, §4.4) yapılabildiği keşifte doğrulanır (madde 10). sözleşmeleri, §4.4) yapılabildiği keşifte doğrulanır (madde 11).
1. **Amaç** — talep neden istenmiş, kim kullanacak, hangi işi çözecek (bir-iki cümle). 1. **Amaç** — talep neden istenmiş, kim kullanacak, hangi işi çözecek (bir-iki cümle).
2. **Görsel analizi** — talepte görsel varsa her biri için (`[image-N]`): ekran türü (liste, form, 2. **Görsel analizi** — talepte görsel varsa her biri için (`[image-N]`): ekran türü (liste, form,
@ -712,31 +717,53 @@ Kısa ve karar odaklı yaz; talebi tekrar anlatma.
analiz satırı `Yetki: {İzin grubu} › {Ara yetki} › {Liste} › {AltForm}` olur. Deploy bunu kendisi analiz satırı `Yetki: {İzin grubu} › {Ara yetki} › {Liste} › {AltForm}` olur. Deploy bunu kendisi
kurar — alt form önce deploy edilir, liste deploy edildiğinde alt formun yetkisi listenin kurar — alt form önce deploy edilir, liste deploy edildiğinde alt formun yetkisi listenin
altına, listenin alt yetkilerinin (Create/Update/…) ardına taşınır; ayrıca bir alan yazılmaz. altına, listenin alt yetkilerinin (Create/Update/…) ardına taşınır; ayrıca bir alan yazılmaz.
Zincir iç içedir: alt formun alt formu bağlı olduğu alt formun altındadır
(`… › HealthCard › HealthCardGeneral › HealthCardWorkHistory`). Kendi menüsü olan ekran başka bir
ekranın sekmesinde de açılıyorsa yetkisi kendi menü dalında kalır; analiz onu alt form ağacında
"menülü" diye işaretler.
Aynı üst menü altındaki ekranlar **bağımlılık sırasıyla** yazılır: başka bir ekranın lookup Aynı üst menü altındaki ekranlar **bağımlılık sırasıyla** yazılır: başka bir ekranın lookup
kaynağı olan (tanım) ekran, onu kullanan ekranın üstündedir. Menüdeki sıra deploy sırasından kaynağı olan (tanım) ekran, onu kullanan ekranın üstündedir. Menüdeki sıra deploy sırasından
gelir; madde 9'daki üretim planı da aynı sırayla yazılır. gelir; madde 10'daki üretim planı da aynı sırayla yazılır.
5. **Veri modeli** — tablo adı (onek kuralı §2), yeni mi var olan mı; kolonlar (ad, tip, zorunluluk, 5. **Veri modeli** — tablo adı (onek kuralı §2), yeni mi var olan mı; kolonlar (ad, tip, zorunluluk,
uzunluk), lookup kaynakları, kiracı + audit (standing default 1). Yeni her tablonun ilk satırı uzunluk), lookup kaynakları, kiracı + audit (standing default 1). Yeni her tablonun ilk satırı
**Tanım tablosu: Evet / Hayır** kararıdır, talepteki dayanağıyla (§8.1); atlanmaz. Evet ise **Tanım tablosu: Evet / Hayır** kararıdır, talepteki dayanağıyla (§8.1); atlanmaz. Evet ise
kolon tablosunda `IsActive` (bit, varsayılan 1, zorunlu) yer alır — talepte ya da görselde kolon tablosunda `IsActive` (bit, varsayılan 1, zorunlu) yer alır — talepte ya da görselde
olmasa da — ve madde 7 (form alanı + Aktif/Pasif widget'ı) ile madde 9 (`create_table` kolonu, olmasa da — ve madde 8 (form alanı + Aktif/Pasif widget'ı) ile madde 10 (`create_table` kolonu,
demo veride `IsActive: true`) bunu taşır. demo veride `IsActive: true`) bunu taşır.
**Görsel kolonu:** görsel tutan her kolon (fotoğraf, resim, logo, avatar kopyası, belge **Görsel kolonu:** görsel tutan her kolon (fotoğraf, resim, logo, avatar kopyası, belge
görüntüsü) `nvarchar(MAX)`'tir ve editörü `dxImageUpload`'dır — değer başka bir kayıttan görüntüsü) `nvarchar(MAX)`'tir ve editörü `dxImageUpload`'dır — değer başka bir kayıttan
kopyalanıyor olsa da. Görsel base64 metin olarak saklanır; `nvarchar(512)` gibi sınırlı boy kopyalanıyor olsa da. Görsel base64 metin olarak saklanır; `nvarchar(512)` gibi sınırlı boy
yazılmaz, `dxImageViewer` kullanılmaz (§4.3 editör tablosu). Çok satırlı serbest metin yazılmaz, `dxImageViewer` kullanılmaz (§4.3 editör tablosu). Çok satırlı serbest metin
(`dxTextArea`, `dxHtmlEditor`) de `nvarchar(MAX)`'tir. (`dxTextArea`, `dxHtmlEditor`) de `nvarchar(MAX)`'tir.
6. **Etkilenen kısımlar** — uygulamada dokunulacak her yer: yeni/değişen ekranlar, alt formlar, **Query kaynağı** (ekran birden çok tabloyu tek sorguda birleştiriyorsa ve view tercih
edilmediyse, §8.2): `SelectCommandType: 4`, `TableName: "Q"`, `KeyFieldName` +
`KeyFieldDbSourceType` zorunlu (§3). Ana tablo (en dıştaki `FROM`'un ilk tablosu) `IsDeleted`
taşıyorsa sorgu ana tablonun `IsDeleted` kolonunu seçer; deploy `DefaultFilter`'ı kendisi yazar.
Query ekranında `DeleteCommand` üretilmez — silme gerekiyorsa hedef tablo kararı **(onay)** ile
yazılır. Join'lerdeki soft delete koşulu (`hc."IsDeleted" = 'false'`) sorgunun içinde durur.
6. **Alt form ağacı** — ekran sekme taşıyorsa her sekme bir satır, kökten iç içe:
| Sekme | Hedef `ListFormCode` | `TabType` | Menü | İlişki | Ekle / Düzenle / Sil | Yetki yolu |
| --- | --- | --- | --- | --- | --- | --- |
| … | … | `Form` (tek kayıt) / `List` (çok kayıt) | Menüsüz / Menülü | `Id → …Id` | ✓ / ✓ / – | `… › Liste › AltForm` |
`Form` sekmesinin hedefi `AllowAdding` + `AllowUpdating` açık yazılır (kayıt yoksa sekme ilk
kaydı açar, varsa ikinci kayıt açılmaz); `List` sekmesinde üçü açıktır; silme yalnızca talep
istiyorsa. Alt form olarak üretilen ekranda (menüsüz ya da menülü) widget yoktur, düzenleme formu
tek gruplu ise grup başlığı yazılmaz
(§4.3). Ayrıntı: §4.6.
7. **Etkilenen kısımlar** — uygulamada dokunulacak her yer: yeni/değişen ekranlar, alt formlar,
menü, yetkiler (`C/R/U/D` + varsa özel), dil anahtarları, widget'lar, iş akışı/bildirim, SEO (public menü, yetkiler (`C/R/U/D` + varsa özel), dil anahtarları, widget'lar, iş akışı/bildirim, SEO (public
yol varsa), var olan ekranlara etkiler. yol varsa), var olan ekranlara etkiler.
7. **Görünümler ve davranış** — Grid/Card/Pivot/Chart (standing default 12), widget kartları (§4.6.1), 8. **Görünümler ve davranış** — Grid/Card/Pivot/Chart (standing default 12), widget kartları (§4.6.1),
`EditorScript` / `EditorOptions` ihtiyaçları, hesaplar ve doğrulamalar. Görsel alanı her zaman `EditorScript` / `EditorOptions` ihtiyaçları, hesaplar ve doğrulamalar. Görsel alanı her zaman
`dxImageUpload`'dır (madde 5). `dxImageUpload`'dır (madde 5). Widget kartları yalnızca ana ekrana yazılır; alt form olarak
8. **Kararlar** — talepten okunamayıp analizde verilen her karar, tek satır gerekçesiyle (§0.6); üretilen ekrana yazılmaz (§4.6.1).
9. **Kararlar** — talepten okunamayıp analizde verilen her karar, tek satır gerekçesiyle (§0.6);
varsayılamayan konular **(onay)** etiketi taşır. "Açık sorular" başlığı yoktur. varsayılamayan konular **(onay)** etiketi taşır. "Açık sorular" başlığı yoktur.
9. **Üretim planı** — bağımlılık sırasıyla (tablo → lookup kaynağı ekran → onu kullanan ekran) sıralı artefakt listesi (`create_table: …`, `deploy_wizard: ….json`, 10. **Üretim planı** — bağımlılık sırasıyla (tablo → lookup kaynağı ekran → en derindeki alt form → onu taşıyan alt form → ana ekran) sıralı artefakt listesi (`create_table: …`, `deploy_wizard: ….json`,
`save_component: …`); üretimdeki `add_dev_request_subtasks` başlıkları bununla aynı olur. `save_component: …`); üretimdeki `add_dev_request_subtasks` başlıkları bununla aynı olur.
10. **Üretime hazır** — tek satır: **Evet**, ya da **Hayır — eksik yetenek: …** (çerçeve dışı 11. **Üretime hazır** — tek satır: **Evet**, ya da **Hayır — eksik yetenek: …** (çerçeve dışı
ihtiyaç, §0.8, adıyla). Talebi daraltan geçici çözüm önerilmez; "Hayır" taşıyan talep yetenek ihtiyaç, §0.8, adıyla). Talebi daraltan geçici çözüm önerilmez; "Hayır" taşıyan talep yetenek
eklenene kadar `Ready to Start`'a alınmaz. eklenene kadar `Ready to Start`'a alınmaz.
@ -810,7 +837,13 @@ yol elle kurulmaz.
**`ComponentKind`** (`WizardComponentKindEnum`) — `0` List · `1` Custom **`ComponentKind`** (`WizardComponentKindEnum`) — `0` List · `1` Custom
**`SelectCommandType`** — `1` Table · `2` View · `3` TableValuedFunction · `4` Query · **`SelectCommandType`** — `1` Table · `2` View · `3` TableValuedFunction · `4` Query ·
`5` StoredProcedure `5` StoredProcedure. `4` Query'de `TableName` zorunludur (sorgunun alias'ı, `FROM (…) AS "Q"`);
wizard/`deploy_wizard` `"Q"` yazar, elle kurulan Query ekranında da doldurulur. `KeyFieldName` +
`KeyFieldDbSourceType` da zorunludur; wizard anahtarsız Query ekranını reddeder, tip `0` gelirse
seçilen alanlardaki karşılığından tamamlar. Sorgunun ana tablosu (en dıştaki `FROM`'un ilk tablosu)
`IsDeleted` taşıyorsa `DefaultFilter` = `"IsDeleted" = 'false'` yazılır; filtre dış sorguda çalıştığı
için sorgu ana tablonun `IsDeleted` kolonunu seçmelidir, seçmiyorsa deploy reddedilir. `DeleteCommand`
Query'de üretilmez (kaynak tablo adı değildir), gerekiyorsa elle yazılır.
**`LookupDataSourceType`** — `1` StaticData · `2` Query · `3` WebService **`LookupDataSourceType`** — `1` StaticData · `2` Query · `3` WebService
@ -1725,6 +1758,39 @@ anahtar) silmeyi durdurmaz: tablo ve scripti yerinde kalır, rapor `Failed` + me
Tablo düşürmek `App.SqlQueryManager` yetkisi ister; wizard silme yetkisi tek başına yetmez. Tablo düşürmek `App.SqlQueryManager` yetkisi ister; wizard silme yetkisi tek başına yetmez.
Düzenleme tabloya hiç dokunmaz. Düzenleme tabloya hiç dokunmaz.
**Silme sırası.** Tek tek silmede sıra sonucu belirler; yanlış sıra kayıtları **sahipsiz** bırakır:
bırakılan (`Kept`) satırın sahibi silinen wizard'dı ve sonradan hiçbir silme onu kaldırmaz.
1. **Alt formlar önce, onları taşıyan ekran sonra** — en derindeki alt form ilk gider
(`HealthCardWorkHistory` → `HealthCardGeneral` → … → `HealthCard`). Bağlı wizard'ı olan wizard
zaten silinemez (yukarıda).
2. **Aynı seviyede en son deploy edilen önce** — paylaşılan menü seviyesi, ara yetki, izin grubu ve
dil anahtarı onu **ilk** üreten wizard'a aittir; o wizard, diğerleri gidip satır kullanımsız
kalınca en son silinirse satır da gider. Önce silinirse satır "kullanımda" diye bırakılır ve
sahipsiz kalır.
3. **Tek tek silmenin sınırı:** alt formun ekranı, onu `SubForms`'unda taşıyan ekran veritabanında
durdukça kullanımda sayılır ve korunur (`Kept`); tablosu da aynı nedenle (`InUse`) düşürülemez.
Kural 1 bu yüzden tek tek silmede alt formun ekranını ve tablosunu temizleyemez — birlikte
üretilmiş bir ekran ağacı **talep olarak** silinir.
**Talep bazlı toplu silme** (Wizard Manager'da talep başlığındaki çöp kutusu; `DeleteDevRequest`,
`DeleteDevRequestAnalysis`, yetki `App.Listforms.Wizard.Delete`) talebin bütün wizard'larını tek
işlemde siler. Onay diyaloğu sırayı ve tabloları gösterir:
- **Sıra** yukarıdaki kural 1 + 2'dir (`BuildDevRequestDeletePlanAsync`); döngüde kalan dosyalar
yalnızca deploy zamanına göre sıralanır. Talebin dışındaki bir alt forma bağlı olmak silmeyi
durdurmaz — o alt form başka bir talebindir ve yerinde kalır.
- **Talebin ekranları birbirini kullanım saymaz:** alt formun ekranı, aynı işte silinecek üst ekran
yüzünden korunmaz (`WizardRecordCleaner.CleanAsync(seed, batchListForms)`); tablo kullanımında da
talebin ekranları sayılmaz (`WizardTableCleaner.AnalyzeAsync(seed, batchListForms)`).
- **Tablolar en sonda** düşürülür: bütün ekranlar gittikten sonra kullanım yeniden ölçülür; aynı
tabloyu paylaşan alt formlar (`Hr_T_HealthCard` gibi) tabloyu bir kez listeler. Bağlantılar veri
kaynağı kaydı silinmeden önce çözülür. Tablo seçmek yine `App.SqlQueryManager` ister.
- Rapor tek wizard silmeyle aynı sözleşmedir (`WizardDeleteResultDto`, bütün wizard'ların kayıtları
tek listede).
MCP `delete_wizard` tek dosya siler ve kural 1 + 2'ye uyar; ekran ağacını temizlemek için kullanıcı
Wizard Manager'daki talep silmesini kullanır.
### 4.6 Alt formlar ve widget'lar ### 4.6 Alt formlar ve widget'lar
**`SubForms` (`SubFormDto`)** — ana kaydın altında sekme olarak açılan ekranlar: **`SubForms` (`SubFormDto`)** — ana kaydın altında sekme olarak açılan ekranlar:
@ -1745,6 +1811,27 @@ Düzenleme tabloya hiç dokunmaz.
`Relation` bir dizidir — bileşik anahtarda birden fazla eşleme verilir. `Relation` bir dizidir — bileşik anahtarda birden fazla eşleme verilir.
**Alt form ekranının kuralları** (sekmede açılan hedef ekran; MCP ile üretimde de geçerli):
- **Menü:** Yalnızca sekmede açılan ekran menüsüz (`CreateMenu: false`) üretilir. Kendi sayfası da
olan ekran (Muayene Listesi gibi) menülü kalır ve ana ekranın `SubForms`'una yalnızca eklenir.
- **Yetki:** Menüsüz alt formun okuma yetkisi ana ekranın yetkisinin altındadır
(`ParentName` = ana ekranın yetki kodu). Zincir iç içedir: alt formun alt formu, bağlı olduğu
alt formun altına yazılır (`App.Hr.HealthCard › App.Hr.HealthCardGeneral › App.Hr.HealthCardWorkHistory`).
Alt formlar ana ekrandan **önce** deploy edilir; ana ekran deploy edildiğinde yetkiler altına
taşınır. Deploy'dan sonra `Abp.Identity.Permissions.json`'da bu bağ doğrulanır — kökte kalan alt form
yetkisi rol ekranında grubun köküne düşer. Menülü ekranın yetkisi kendi menü dalında kalır.
- **`TabType: "Form"` — ana kayda bağlı tek kayıt:** Sekme ilişkiye uyan tek kaydı gösterir (1:1,
ör. çalışan başına bir sağlık kartı bölümü). Hedef ekran `AllowAdding: true` ve
`AllowUpdating: true` ile üretilir: kayıt yokken sekme "Yeni" düğmesi gösterir, kayıt varken
yalnızca düzenleme açıktır ve ikinci kayıt açılmaz (`FormView` + `FormButtons`, `isSubForm`).
`AllowAdding` kapalıysa sekme boş "kayıt bulunamadı" mesajında kalır ve kayıt girilemez.
`AllowDeleting` istek açıkça istemedikçe kapalıdır.
- **`TabType: "List"` — ana kayda bağlı çok kayıt:** `AllowAdding`/`AllowUpdating`/`AllowDeleting`
açık gelir; ilişki kolonu (`ChildFieldName`) yeni satıra ana kayıttan yazılır.
- **Widget:** Alt form oluşturulurken widget oluşturulmaz — menüsüz ya da menülü, alt form olarak
üretilen her ekran `Widgets: []` ile gider; sekmede şerit ayrıca çizilmez (§4.6.1).
**`Widgets` (`WidgetEditDto`)** — ekranın üstündeki KPI kartları. **`Title`, `SubTitle`, **`Widgets` (`WidgetEditDto`)** — ekranın üstündeki KPI kartları. **`Title`, `SubTitle`,
`Value`, `Icon`, `Color`, `OnClick` sabit metin değil, `SqlQuery` sonucundaki kolon adlarıdır**; `Value`, `Icon`, `Color`, `OnClick` sabit metin değil, `SqlQuery` sonucundaki kolon adlarıdır**;
widget, sorgunun döndürdüğü **her satır için bir kart** çizer: widget, sorgunun döndürdüğü **her satır için bir kart** çizer:
@ -1814,11 +1901,19 @@ gelir; kullanıcı "widget/KPI" demese de, sorulmadan eklenir ve tarifte tek sat
- **Renk ve ikon:** `blue` toplam, `green` olumlu/aktif, `gray` pasif/taslak, `red` hata/iptal, - **Renk ve ikon:** `blue` toplam, `green` olumlu/aktif, `gray` pasif/taslak, `red` hata/iptal,
`yellow`/`orange` bekleyen, `purple` tutar/özet. İkon `react-icons/fa` adıdır (`FaCheckCircle`, `yellow`/`orange` bekleyen, `purple` tutar/özet. İkon `react-icons/fa` adıdır (`FaCheckCircle`,
`FaTimesCircle`, `FaCalendarDay`, `FaEye`, `FaMoneyBillWave`…). `FaTimesCircle`, `FaCalendarDay`, `FaEye`, `FaMoneyBillWave`…).
- **İstisna — widget eklenmez:** Form ve Chart ekranları; alt form olarak kullanılan menüsüz - **İstisna — widget eklenmez:** Form ve Chart ekranları; **alt form olarak üretilen her ekran** —
ekranlar (`CreateMenu: false`, beğeni/yorum/seçenek gibi detay tabloları); `IsActive` de başka bir ekranın `SubForms` sekmesi olan, menüsüz (`CreateMenu: false`, beğeni/yorum/seçenek gibi
detay tabloları) ya da kendi menüsü de olan (Muayene Listesi gibi) — `Widgets: []` ile deploy
edilir; MCP bu ekranlara widget üretmez, menüsüz olanda deploy gönderileni de düşürür; `IsActive` de
taşımayan, yalnızca kod + ad tutan eski tanım listeleri. Bunlarda anlamlı bir kırılım yoktur, taşımayan, yalnızca kod + ad tutan eski tanım listeleri. Bunlarda anlamlı bir kırılım yoktur,
şerit yalnızca kayıt sayısını tekrarlar. Yeni tanım tablosu `IsActive` ile gelir (§8.1) ve şerit yalnızca kayıt sayısını tekrarlar. Yeni tanım tablosu `IsActive` ile gelir (§8.1) ve
Toplam · Aktif · Pasif kartlarını alır. Toplam · Aktif · Pasif kartlarını alır.
- **Alt form sekmesinde widget çizilmez.** Widget sorgusu sekmede `Relation` ile süzülmez; kartlar
sekmedeki satırlarla değil bütün tabloyla ilgili sayılar gösterirdi. Liste görünümleri alt form
olarak açıldığında şeridi çizmez ve yenilemez (`useListFormWidgets(…, isSubForm)`, Pivot'ta
`isSubForm`); kendi menüsü olan ekranın şeridi kendi sayfasında kalır. Bu bir güvencedir, üretim
kuralının yerine geçmez: alt form olarak üretilen ekrana yine widget yazılmaz. Ana kayda göre
sayı isteniyorsa kart ana ekranın widget'ına yazılır.
- Kartların sorgusunu seed dosyasına yazmadan önce tablo kolonları doğrulanır - Kartların sorgusunu seed dosyasına yazmadan önce tablo kolonları doğrulanır
(MCP'de `search_schema`, ekranda tablo tasarımcısı). Var olmayan kolona yazılmış kart ekranı hatayla açar. (MCP'de `search_schema`, ekranda tablo tasarımcısı). Var olmayan kolona yazılmış kart ekranı hatayla açar.
@ -1924,8 +2019,8 @@ olmalıdır.
| --- | --- | | --- | --- |
| `DataSourceCode` | Bağlantı | | `DataSourceCode` | Bağlantı |
| `SelectCommandType` + `SelectCommand` | Kaynak türü ve kaynağın kendisi | | `SelectCommandType` + `SelectCommand` | Kaynak türü ve kaynağın kendisi |
| `TableName` | Tablo adı/alias — yazma komutlarının hedefi | | `TableName` | Tablo adı/alias — yazma komutlarının hedefi; `4` Query'de zorunlu alias (`"Q"`) |
| `KeyFieldName` + `KeyFieldDbSourceType` | Anahtar | | `KeyFieldName` + `KeyFieldDbSourceType` | Anahtar; `4` Query'de ikisi de zorunlu |
| `SelectFieldsDefaultValueJson` | Select'e geçirilen varsayılan parametreler | | `SelectFieldsDefaultValueJson` | Select'e geçirilen varsayılan parametreler |
| `DefaultFilter` | Her sorgunun sonuna eklenen WHERE (soft delete filtresi buradadır) | | `DefaultFilter` | Her sorgunun sonuna eklenen WHERE (soft delete filtresi buradadır) |
| `IsTenant` · `IsBranch` · `IsOrganizationUnit` | Otomatik kırılım filtreleri | | `IsTenant` · `IsBranch` · `IsOrganizationUnit` | Otomatik kırılım filtreleri |
@ -4410,6 +4505,9 @@ Prompt ile üretim yaptıysan ek olarak:
- [ ] Liste ekranında `Grid`/`Card`/`Pivot`/`Chart` açık, `DefaultLayout: "grid"`; kapalı olan - [ ] Liste ekranında `Grid`/`Card`/`Pivot`/`Chart` açık, `DefaultLayout: "grid"`; kapalı olan
görünüm yalnızca istek onu açıkça dışarıda bıraktığı için kapalı (§9.8). görünüm yalnızca istek onu açıkça dışarıda bıraktığı için kapalı (§9.8).
- [ ] Açılan her görünümün zorunlu option alanı dolu (§9.8). - [ ] Açılan her görünümün zorunlu option alanı dolu (§9.8).
- [ ] Alt form olarak üretilen her ekranda (menüsüz ya da menülü) `Widgets` boş (§4.6.1 istisna).
- [ ] Menüsüz alt formun yetkisi ana ekranın (ya da bağlı olduğu alt formun) yetkisinin altında;
`Form` sekmesinin hedefi `AllowAdding` + `AllowUpdating` açık (§4.6).
- [ ] List ekranında en az 3 kartlık `Widgets` var (istisna değilse); kiracı tablosunda her kart - [ ] List ekranında en az 3 kartlık `Widgets` var (istisna değilse); kiracı tablosunda her kart
`"TenantId" = '@TENANTID'` taşıyor, başlıklar dil anahtarı, kolonlar doğrulandı (§4.6.1). `"TenantId" = '@TENANTID'` taşıyor, başlıklar dil anahtarı, kolonlar doğrulandı (§4.6.1).
- [ ] Custom component'te route yetkisiz üretildiği için koruma bileşenin içinde - [ ] Custom component'te route yetkisiz üretildiği için koruma bileşenin içinde

View file

@ -375,7 +375,7 @@ Ekran davranışı büyük ölçüde JSON kolonlarında saklanır. Öne çıkanl
| `SeedFilePath` | Doluysa seed sırasında listenin verisi kapsam klasöründeki bu hedeften geri yüklenir: `.json` ile biten değer tek dosya (`data/{ListFormCode}.json`), uzantısız değer satır başına dosya tutan klasördür (`data/{ListFormCode}/{anahtar}.json`). | | `SeedFilePath` | Doluysa seed sırasında listenin verisi kapsam klasöründeki bu hedeften geri yüklenir: `.json` ile biten değer tek dosya (`data/{ListFormCode}.json`), uzantısız değer satır başına dosya tutan klasördür (`data/{ListFormCode}/{anahtar}.json`). |
| `SeedSyncInsert` / `SeedSyncUpdate` / `SeedSyncDelete` | Ekranda yapılan hangi işlemin seed dosyasına yansıyacağı; kapalıysa o işlem dosyaya dokunmaz. | | `SeedSyncInsert` / `SeedSyncUpdate` / `SeedSyncDelete` | Ekranda yapılan hangi işlemin seed dosyasına yansıyacağı; kapalıysa o işlem dosyaya dokunmaz. |
| `SubFormsJson` | Ana-detay ilişkisi (ParentFieldName → ChildFieldName eşlemesi). | | `SubFormsJson` | Ana-detay ilişkisi (ParentFieldName → ChildFieldName eşlemesi). |
| `WidgetsJson` | Ekranın üstünde gösterilecek KPI kartları. | | `WidgetsJson` | Ekranın üstünde gösterilecek KPI kartları. Alt form olarak üretilen ekrana (menüsüz ya da menülü) yazılmaz; menüsüz wizard'da deploy boşaltır, alt form sekmesinde çizilmez. |
| `WorkflowJson` | İş akışı bağlaması. | | `WorkflowJson` | İş akışı bağlaması. |
| `ExtraFilterJson` | Ekrana özel ek filtre araç çubuğu. | | `ExtraFilterJson` | Ekrana özel ek filtre araç çubuğu. |
| `PivotOptionJson`, `TreeOptionJson`, `GanttOptionJson`, `SchedulerOptionJson`, `TodoOptionJson` | Alternatif görünümlerin ayarları. | | `PivotOptionJson`, `TreeOptionJson`, `GanttOptionJson`, `SchedulerOptionJson`, `TodoOptionJson` | Alternatif görünümlerin ayarları. |
@ -754,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. 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}`. | | **Wizard Manager** | `/admin/listform/wizardManager` | Üretilmiş wizard seed dosyalarının listesi/kartı, düzenleme, silme, DB migrate, export/import. Menüsüz alt formlar onları sekme olarak açan ekranın altında iç içe görünür; kendi menüsü olan ekran başka bir ekranın sekmesi olsa da kökte kalır (export kapanışı onu yine taşır). 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`) ya da bağımlılık sırasıyla tek işlemde silinir (`delete-dev-request`; alt formlar önce, talebin ekranları birbirini kullanım saymaz, tablolar en sonda — `lowcode.instructions.md` §4.5). 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)

View file

@ -15,6 +15,12 @@ public interface IListFormWizardAppService
/// <summary>Silme onayinda sorulacak tablolari ve tabloyla birlikte gidecek seed dosyalarini dondurur.</summary> /// <summary>Silme onayinda sorulacak tablolari ve tabloyla birlikte gidecek seed dosyalarini dondurur.</summary>
Task<WizardDeleteAnalysisDto> DeleteAnalysis(string fileName); Task<WizardDeleteAnalysisDto> DeleteAnalysis(string fileName);
/// <summary>Bir gelistirme talebinin butun wizard'larinin silinme sirasi ve secilebilen tablolari.</summary>
Task<WizardDeleteDevRequestAnalysisDto> DeleteDevRequestAnalysis(Guid devRequestId);
/// <summary>Bir gelistirme talebinin butun wizard'larini bagimlilik sirasiyla tek islemde siler.</summary>
Task<WizardDeleteResultDto> DeleteDevRequest(WizardDeleteDevRequestInput input);
/// <summary>Izin gruplarini iki dilli gorunen adlariyla dondurur.</summary> /// <summary>Izin gruplarini iki dilli gorunen adlariyla dondurur.</summary>
Task<List<WizardPermissionGroupDto>> GetPermissionGroups(); Task<List<WizardPermissionGroupDto>> GetPermissionGroups();

View file

@ -253,10 +253,12 @@ public class ListFormWizardDto
IsOrganizationUnit = IsOrganizationUnit, IsOrganizationUnit = IsOrganizationUnit,
SelectCommandType = SelectCommandType, SelectCommandType = SelectCommandType,
SelectCommand = SelectCommand, SelectCommand = SelectCommand,
TableName = SelectCommandType == SelectCommandTypeEnum.Query ? WizardConsts.QuerySourceAlias : null,
KeyFieldName = KeyFieldName, KeyFieldName = KeyFieldName,
KeyFieldDbSourceType = KeyFieldDbSourceType, KeyFieldDbSourceType = KeyFieldDbSourceType,
DefaultFilter = isDeletedField ? WizardConsts.DefaultFilterJson : null, DefaultFilter = isDeletedField ? WizardConsts.DefaultFilterJson : null,
DeleteCommand = isDeletedField ? WizardConsts.DefaultDeleteCommand(SelectCommand) : null, // Query'de SelectCommand tablo adi degildir; silme komutu elle yazilir.
DeleteCommand = isDeletedField && SelectCommandType != SelectCommandTypeEnum.Query ? WizardConsts.DefaultDeleteCommand(SelectCommand) : null,
SeedFilePath = SeedData ? WizardConsts.SeedDataFilePath(listFormCode) : null, SeedFilePath = SeedData ? WizardConsts.SeedDataFilePath(listFormCode) : null,
SeedSyncInsert = SeedData, SeedSyncInsert = SeedData,
SeedSyncUpdate = SeedData, SeedSyncUpdate = SeedData,

View file

@ -532,6 +532,31 @@ public class WizardDeleteInput
public List<string> DropTables { get; set; } = []; public List<string> DropTables { get; set; } = [];
} }
/// <summary>Talebin wizard'lari silinme sirasiyla; alt formlar onlari tasiyan ekrandan once gelir.</summary>
public class WizardDeleteDevRequestAnalysisDto
{
public Guid DevRequestId { get; set; }
public string Title { get; set; }
public List<WizardDeleteOrderItemDto> Files { get; set; } = [];
/// <summary>Talebin disindaki ekranlar hesaba katilarak secilebilen tablolar; ayni tablo bir kez yer alir.</summary>
public List<WizardDeleteTableDto> Tables { get; set; } = [];
}
public class WizardDeleteOrderItemDto
{
public string FileName { get; set; }
public string WizardName { get; set; }
}
public class WizardDeleteDevRequestInput
{
public Guid DevRequestId { get; set; }
/// <summary>Onayda secilen tablolar; yalnizca analizde <see cref="WizardDeleteTableDto.CanDrop"/> olanlar dusurulur.</summary>
public List<string> DropTables { get; set; } = [];
}
/// <summary>Wizard seed dosyası özet bilgisi (yönetim listesi için).</summary> /// <summary>Wizard seed dosyası özet bilgisi (yönetim listesi için).</summary>
public class WizardFileInfoDto public class WizardFileInfoDto
{ {

View file

@ -125,6 +125,12 @@ public class ListFormWizardAppService(
// Ad ve kod catismalari, dosyalar yazilmaya baslamadan once reddedilir; aksi halde yarim // Ad ve kod catismalari, dosyalar yazilmaya baslamadan once reddedilir; aksi halde yarim
// kalan bir deploy mevcut menu/permission satirlarini bozabilir. // kalan bir deploy mevcut menu/permission satirlarini bozabilir.
ValidateWizardName(wizardName); ValidateWizardName(wizardName);
ValidateQueryKeyField(input);
// Menusuz ekran yalnizca baska bir ekranin alt formu olarak acilir; orada widget cizilmez
// ve sorgusu ana kayda gore suzulmedigi icin kartlar yaniltici sayilar gosterirdi.
if (!input.CreateMenu)
input.Widgets = [];
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);
@ -230,7 +236,9 @@ public class ListFormWizardAppService(
// Ekranin sozlesmesi tek fabrikadan uretilir; migrate ekrani bu dosyadan kurar. // Ekranin sozlesmesi tek fabrikadan uretilir; migrate ekrani bu dosyadan kurar.
var tableColumns = await GetTableColumnsAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand); var tableColumns = await GetTableColumnsAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand);
isDeleted = tableColumns.ContainsKey("IsDeleted"); isDeleted = input.SelectCommandType == SelectCommandTypeEnum.Query
? await IsQueryMainTableSoftDeletedAsync(input)
: tableColumns.ContainsKey("IsDeleted");
isCreated = tableColumns.ContainsKey("CreatorId"); isCreated = tableColumns.ContainsKey("CreatorId");
ValidateImageColumnLengths(input, tableColumns); ValidateImageColumnLengths(input, tableColumns);
listFormDto = input.BuildListForm(isDeleted, isCreated); listFormDto = input.BuildListForm(isDeleted, isCreated);
@ -1029,6 +1037,146 @@ public class ListFormWizardAppService(
}; };
} }
/// <summary>
/// Talebin silinme sirasi ve onayda sorulacak tablolar; hicbir seye dokunmaz. Tablolarin
/// kullanim kontrolu talebin kendi ekranlarini disarida birakir, hepsi ayni islemde gider.
/// </summary>
[Authorize(AppCodes.Listforms.Wizards.Delete)]
[HttpGet("api/app/list-form-wizard/delete-dev-request-analysis")]
public async Task<WizardDeleteDevRequestAnalysisDto> DeleteDevRequestAnalysis(Guid devRequestId)
{
var plan = await BuildDevRequestDeletePlanAsync(devRequestId);
return new WizardDeleteDevRequestAnalysisDto
{
DevRequestId = devRequestId,
Title = plan.Title,
Files = [.. plan.Files.Select(f => new WizardDeleteOrderItemDto
{
FileName = f.FileName,
WizardName = f.Seed.Wizard?.WizardName ?? f.FileName,
})],
Tables = await AnalyzeBatchTablesAsync(plan),
};
}
/// <summary>
/// Talebin butun wizard'larini tek tek silmenin yol actigi yarim temizligi onler. Tek tek
/// silmede alt form, onu tasiyan ekran hala durdugu icin kullanimda sayilip korunur; ekran
/// sonradan silinince alt formun kayitlari ve tablosu sahipsiz kalir. Burada wizard'lar
/// bagimlilik sirasiyla silinir (<see cref="BuildDevRequestDeletePlanAsync"/>), talebin kendi
/// ekranlari birbirini kullanim saymaz ve tablolar ekranlar gittikten sonra dusurulur.
/// </summary>
[UnitOfWork]
[Authorize(AppCodes.Listforms.Wizards.Delete)]
[HttpPost("api/app/list-form-wizard/delete-dev-request")]
public async Task<WizardDeleteResultDto> DeleteDevRequest(WizardDeleteDevRequestInput input)
{
Check.NotNull(input, nameof(input));
var dropTables = input.DropTables?.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t.Trim()).ToList() ?? [];
if (dropTables.Count > 0)
await AuthorizationService.CheckAsync(AppCodes.SqlQueryManagers.Default);
var plan = await BuildDevRequestDeletePlanAsync(input.DevRequestId);
// Baglantilar, wizard'in kurdugu veri kaynagi kaydi silinmeden once cozulur.
var before = await AnalyzeBatchTablesAsync(plan);
var connections = await _tableCleaner.ResolveConnectionsAsync(
before.Where(t => dropTables.Contains(t.TableName, StringComparer.OrdinalIgnoreCase)));
var result = new WizardDeleteResultDto { FileName = input.DevRequestId.ToString(), WizardName = plan.Title };
foreach (var (fileName, _) in plan.Files)
result.Items.AddRange((await DeleteFileInternalAsync(fileName, batchListForms: plan.ListFormCodes)).Items);
// Ekranlar gittikten sonra kullanim yeniden olculur; veri kaynagi silinmis olabilecegi icin
// tablonun varligi ilk analizden alinir.
var tables = await AnalyzeBatchTablesAsync(plan);
foreach (var table in tables)
{
var first = before.FirstOrDefault(t => SameTable(t, table));
if (first is { Exists: true } && !table.Exists)
{
table.Exists = true;
table.RowCount = first.RowCount;
table.SchemaName = first.SchemaName;
}
}
if (tables.Count > 0)
await _tableCleaner.DropAsync(tables, dropTables, connections, result.Items);
return result;
}
private sealed record DevRequestDeletePlan(string Title, List<(string FileName, WizardSeedFileDto Seed)> Files, List<string> ListFormCodes);
/// <summary>
/// Talebin wizard'lari silinme sirasiyla. Alt formlar onlari tasiyan ekrandan once gelir
/// (tek tek silmedeki kuralla ayni, <see cref="EnsureNoDependentWizardsAsync"/>); siralanabilen
/// adaylar arasinda en son deploy edilen once silinir. Paylasilan menu seviyesi, ara yetki ve dil
/// anahtari onu ilk ureten wizard'a aittir; digerleri gittikten sonra kullanimsiz kalip silinir.
/// </summary>
private async Task<DevRequestDeletePlan> BuildDevRequestDeletePlanAsync(Guid devRequestId)
{
var members = (await ReadSeedFilesAsync())
.Where(f => f.Seed?.Wizard != null && f.Seed.DevRequest?.Id == devRequestId)
.Select(f => (f.FileName, Seed: f.Seed!))
.ToList();
if (members.Count == 0)
throw new UserFriendlyException("Bu talebe bağlı wizard dosyası yok.");
var dependencies = (await GetFiles()).ToDictionary(f => f.FileName, f => f.Dependencies, StringComparer.OrdinalIgnoreCase);
var remaining = members.ToList();
var ordered = new List<(string FileName, WizardSeedFileDto Seed)>();
bool IsReady((string FileName, WizardSeedFileDto Seed) file) =>
!(dependencies.GetValueOrDefault(file.FileName) ?? [])
.Any(d => remaining.Any(r => string.Equals(r.FileName, d, StringComparison.OrdinalIgnoreCase)));
while (remaining.Count > 0)
{
// Donguye giren dosyalarin hicbiri hazir olmaz; o durumda sira yalnizca deploy zamanina duser.
var ready = remaining.Where(IsReady).ToList();
var next = (ready.Count > 0 ? ready : remaining).OrderByDescending(f => f.Seed.Wizard.SeededAt).First();
ordered.Add(next);
remaining.Remove(next);
}
var devRequest = await repoDevRequest.FindAsync(devRequestId);
var title = devRequest?.Title ?? members[0].Seed.DevRequest.Title;
var listFormCodes = members
.Where(f => f.Seed.Wizard.ComponentKind != WizardComponentKindEnum.Custom)
.Select(f => f.Seed.Wizard.ResolveListFormCode())
.ToList();
return new DevRequestDeletePlan(title, ordered, listFormCodes);
}
/// <summary>Talebin ekranlarinin tablolari, ayni tablo bir kez; bir ekran icin secilemeyen tablo secilemez kalir.</summary>
private async Task<List<WizardDeleteTableDto>> AnalyzeBatchTablesAsync(DevRequestDeletePlan plan)
{
var tables = new List<WizardDeleteTableDto>();
foreach (var (_, seed) in plan.Files)
{
foreach (var table in await _tableCleaner.AnalyzeAsync(seed, plan.ListFormCodes))
{
var existing = tables.FirstOrDefault(t => SameTable(t, table));
if (existing == null)
tables.Add(table);
else if (existing.CanDrop && !table.CanDrop)
tables[tables.IndexOf(existing)] = table;
}
}
return tables;
}
private static bool SameTable(WizardDeleteTableDto a, WizardDeleteTableDto b) =>
string.Equals(a.DataSourceCode, b.DataSourceCode, StringComparison.OrdinalIgnoreCase) &&
string.Equals(a.TableName, b.TableName, StringComparison.OrdinalIgnoreCase);
/// <summary> /// <summary>
/// Bagli wizard'lari (alt formlar / component bagimliliklari) olan bir wizard silinemez; once /// Bagli wizard'lari (alt formlar / component bagimliliklari) olan bir wizard silinemez; once
/// bagli olanlar silinir. Kural listedeki gruplamayla ayni kaynaktandir /// bagli olanlar silinir. Kural listedeki gruplamayla ayni kaynaktandir
@ -1060,7 +1208,11 @@ public class ListFormWizardAppService(
/// satirlari cikarilir. Veritabani adimi basarisizsa dosyalara dokunulmaz.</item> /// satirlari cikarilir. Veritabani adimi basarisizsa dosyalara dokunulmaz.</item>
/// </list> /// </list>
/// </summary> /// </summary>
private async Task<WizardDeleteResultDto> DeleteFileInternalAsync(string fileName, List<string>? dropTables = null) /// <param name="batchListForms">
/// Toplu silmede ayni islemde silinen ekranlar (<see cref="DeleteDevRequest"/>). Verildiginde
/// tablolara dokunulmaz; toplu silme onlari ekranlar gittikten sonra kendisi dusurur.
/// </param>
private async Task<WizardDeleteResultDto> DeleteFileInternalAsync(string fileName, List<string>? dropTables = null, IReadOnlyCollection<string>? batchListForms = null)
{ {
var filePath = ResolveWizardSeedFilePath(fileName); var filePath = ResolveWizardSeedFilePath(fileName);
@ -1075,7 +1227,7 @@ public class ListFormWizardAppService(
// Tablolar ekran silinmeden once analiz edilir: kullanim kontrolu ekranin kendisini disarida // Tablolar ekran silinmeden once analiz edilir: kullanim kontrolu ekranin kendisini disarida
// birakir ve baglanti, veri kaynagi kaydi temizlenmeden once cozulur. // birakir ve baglanti, veri kaynagi kaydi temizlenmeden once cozulur.
var tables = seed != null var tables = seed != null && batchListForms == null
? await _tableCleaner.AnalyzeAsync(seed) ? await _tableCleaner.AnalyzeAsync(seed)
: []; : [];
var connections = await _tableCleaner.ResolveConnectionsAsync( var connections = await _tableCleaner.ResolveConnectionsAsync(
@ -1091,7 +1243,7 @@ public class ListFormWizardAppService(
List<WizardDeleteLogItemDto> items; List<WizardDeleteLogItemDto> items;
using (var uow = UnitOfWorkManager.Begin(new AbpUnitOfWorkOptions(isTransactional: true), requiresNew: true)) using (var uow = UnitOfWorkManager.Begin(new AbpUnitOfWorkOptions(isTransactional: true), requiresNew: true))
{ {
items = await _recordCleaner.CleanAsync(seed); items = await _recordCleaner.CleanAsync(seed, batchListForms);
await uow.CompleteAsync(); await uow.CompleteAsync();
} }
@ -2676,6 +2828,96 @@ public class ListFormWizardAppService(
} }
} }
/// <summary>
/// Query kaynaginin kolonlari katalogdan okunamaz; anahtar alan ve tipi yalnizca girdiden gelir.
/// Anahtarsiz ekranda sayim, siralama ve satir secimi bozulur. Tip bos (0) gelirse secilen
/// alanlardaki karsiligindan tamamlanir.
/// </summary>
private static void ValidateQueryKeyField(ListFormWizardDto input)
{
if (input.ComponentKind == WizardComponentKindEnum.Custom || input.SelectCommandType != SelectCommandTypeEnum.Query)
return;
if (string.IsNullOrWhiteSpace(input.KeyFieldName))
throw new UserFriendlyException("Query kaynaklı ekranda anahtar alan (KeyFieldName) zorunludur.");
input.KeyFieldName = input.KeyFieldName.Trim();
if (input.KeyFieldDbSourceType != default)
return;
var keyItem = (input.Groups ?? [])
.SelectMany(g => g.Items)
.FirstOrDefault(i => string.Equals(i.FieldName, input.KeyFieldName, StringComparison.OrdinalIgnoreCase));
if (keyItem == null || keyItem.DbSourceType == default)
throw new UserFriendlyException($"Query kaynaklı ekranda '{input.KeyFieldName}' anahtar alanının tipi (KeyFieldDbSourceType) zorunludur.");
input.KeyFieldDbSourceType = keyItem.DbSourceType;
}
/// <summary>
/// Query kaynaginin ana tablosu (en distaki FROM'un ilk tablosu) soft delete tasiyorsa silinen
/// satirlar listeye dusmesin diye DefaultFilter yazilir. Filtre turetilmis tablonun disinda
/// calistigi icin sorgu IsDeleted kolonunu secmek zorundadir; secmiyorsa deploy reddedilir.
/// </summary>
private async Task<bool> IsQueryMainTableSoftDeletedAsync(ListFormWizardDto input)
{
var (selectList, mainTable) = SplitTopLevelQuery(input.SelectCommand);
if (mainTable == null)
return false;
var columns = await GetTableColumnsAsync(input.DataSourceCode, SelectCommandTypeEnum.Table, mainTable);
if (!columns.ContainsKey("IsDeleted"))
return false;
if (!Regex.IsMatch(selectList, @"\bIsDeleted\b", RegexOptions.IgnoreCase))
throw new UserFriendlyException($"Query kaynağının ana tablosu '{mainTable}' soft delete taşıyor; silinen satırların listelenmemesi için sorgu ana tablonun IsDeleted kolonunu seçmelidir.");
return true;
}
/// <summary>
/// Sorgunun en dis seviyedeki select listesi ve FROM'dan sonraki ilk tablo. Alt sorgu ve metin
/// sabitlerindeki FROM sayilmaz; kaynak turetilmis tabloysa ana tablo yoktur.
/// </summary>
private static (string SelectList, string? MainTable) SplitTopLevelQuery(string sql)
{
if (string.IsNullOrWhiteSpace(sql))
return (string.Empty, null);
var depth = 0;
var inLiteral = false;
for (var i = 0; i < sql.Length; i++)
{
var c = sql[i];
if (c == '\'')
inLiteral = !inLiteral;
else if (inLiteral)
continue;
else if (c == '(')
depth++;
else if (c == ')')
depth--;
else if (depth == 0 && IsKeywordAt(sql, i, "FROM"))
{
var match = Regex.Match(sql[(i + 4)..], @"^\s+([\w\.\[\]""]+)");
return (sql[..i], match.Success ? match.Groups[1].Value : null);
}
}
return (sql, null);
}
private static bool IsKeywordAt(string sql, int index, string keyword)
{
if (index + keyword.Length > sql.Length
|| string.Compare(sql, index, keyword, 0, keyword.Length, StringComparison.OrdinalIgnoreCase) != 0)
return false;
var before = index == 0 ? ' ' : sql[index - 1];
var after = index + keyword.Length == sql.Length ? ' ' : sql[index + keyword.Length];
return !char.IsLetterOrDigit(before) && before != '_' && !char.IsLetterOrDigit(after) && after != '_';
}
/// <summary>Tablonun kolonlari ve metin kolonlarinin karakter siniri (sinirsizsa -1 ya da bos).</summary> /// <summary>Tablonun kolonlari ve metin kolonlarinin karakter siniri (sinirsizsa -1 ya da bos).</summary>
private async Task<Dictionary<string, int?>> GetTableColumnsAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand) private async Task<Dictionary<string, int?>> GetTableColumnsAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand)
{ {

View file

@ -64,7 +64,11 @@ public class WizardRecordCleaner(
/// <summary>Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.</summary> /// <summary>Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.</summary>
private const int MaxUsedBy = 20; private const int MaxUsedBy = 20;
public async Task<List<WizardDeleteLogItemDto>> CleanAsync(WizardSeedFileDto seed) /// <param name="batchListForms">
/// Ayni islemde silinen ekranlar; onlarin alt form baglari bu ekrani kullanim saymaz. Toplu
/// silmede alt form, onu tasiyan ekrandan once silinir ve bag o ana kadar durur.
/// </param>
public async Task<List<WizardDeleteLogItemDto>> CleanAsync(WizardSeedFileDto seed, IReadOnlyCollection<string>? batchListForms = null)
{ {
var report = new List<WizardDeleteLogItemDto>(); var report = new List<WizardDeleteLogItemDto>();
var wizard = seed.Wizard; var wizard = seed.Wizard;
@ -72,11 +76,11 @@ public class WizardRecordCleaner(
// Custom yolunda ListForm uretilmez; ayni kodu tasiyan bir List wizard'inin ekrani silinmesin. // Custom yolunda ListForm uretilmez; ayni kodu tasiyan bir List wizard'inin ekrani silinmesin.
if (wizard?.ComponentKind != WizardComponentKindEnum.Custom && !string.IsNullOrWhiteSpace(wizard?.ListFormCode)) if (wizard?.ComponentKind != WizardComponentKindEnum.Custom && !string.IsNullOrWhiteSpace(wizard?.ListFormCode))
await CleanListFormAsync(wizard.ListFormCode, report); await CleanListFormAsync(wizard.ListFormCode, report, batchListForms);
// Migrate edilmeden kodu degistirilen ekranin onceki surumu hala kuruludur. // Migrate edilmeden kodu degistirilen ekranin onceki surumu hala kuruludur.
foreach (var listFormCode in seed.Pending?.ListFormCodes ?? []) foreach (var listFormCode in seed.Pending?.ListFormCodes ?? [])
await CleanListFormAsync(listFormCode, report); await CleanListFormAsync(listFormCode, report, batchListForms);
var deletedMenus = await CleanMenusAsync(ins.MenuCodes, report); var deletedMenus = await CleanMenusAsync(ins.MenuCodes, report);
var adoptedLanguageKeys = new List<string>(); var adoptedLanguageKeys = new List<string>();
@ -123,7 +127,7 @@ public class WizardRecordCleaner(
/// Ekran baska bir ekranin alt formuysa silinmez; alanlari ve is akisi da ekranla birlikte /// Ekran baska bir ekranin alt formuysa silinmez; alanlari ve is akisi da ekranla birlikte
/// kalir, aksi halde ust ekranin sekmesi bozulurdu. /// kalir, aksi halde ust ekranin sekmesi bozulurdu.
/// </summary> /// </summary>
private async Task CleanListFormAsync(string listFormCode, List<WizardDeleteLogItemDto> report) private async Task CleanListFormAsync(string listFormCode, List<WizardDeleteLogItemDto> report, IReadOnlyCollection<string>? batchListForms)
{ {
var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode); var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
var fields = await repoListFormField.GetListAsync(a => a.ListFormCode == listFormCode); var fields = await repoListFormField.GetListAsync(a => a.ListFormCode == listFormCode);
@ -136,8 +140,10 @@ public class WizardRecordCleaner(
else else
{ {
var quoted = Quote(listFormCode); var quoted = Quote(listFormCode);
var parents = await repoListForm.GetListAsync(a => var parents = (await repoListForm.GetListAsync(a =>
a.ListFormCode != listFormCode && a.SubFormsJson != null && a.SubFormsJson.Contains(quoted)); a.ListFormCode != listFormCode && a.SubFormsJson != null && a.SubFormsJson.Contains(quoted)))
.Where(p => batchListForms?.Contains(p.ListFormCode, StringComparer.OrdinalIgnoreCase) != true)
.ToList();
if (parents.Count > 0) if (parents.Count > 0)
{ {

View file

@ -43,7 +43,8 @@ public class WizardTableCleaner(
{ {
private const int MaxUsedBy = 20; private const int MaxUsedBy = 20;
public async Task<List<WizardDeleteTableDto>> AnalyzeAsync(WizardSeedFileDto seed) /// <param name="batchListForms">Ayni islemde silinen ekranlar; tabloyu kullanmalari engel sayilmaz.</param>
public async Task<List<WizardDeleteTableDto>> AnalyzeAsync(WizardSeedFileDto seed, IReadOnlyCollection<string>? batchListForms = null)
{ {
var wizard = seed?.Wizard; var wizard = seed?.Wizard;
if (wizard == null || wizard.ComponentKind == WizardComponentKindEnum.Custom) if (wizard == null || wizard.ComponentKind == WizardComponentKindEnum.Custom)
@ -76,7 +77,10 @@ public class WizardTableCleaner(
await ReadTableStateAsync(table); await ReadTableStateAsync(table);
table.SchemaName ??= "dbo"; table.SchemaName ??= "dbo";
var usedBy = await FindUsagesAsync(table, listFormCode); var usedBy = (await FindUsagesAsync(table, listFormCode))
.Where(u => u.RecordType != WizardRecordTypeEnum.ListForm ||
batchListForms?.Contains(u.Name, StringComparer.OrdinalIgnoreCase) != true)
.ToList();
if (usedBy.Count > 0) if (usedBy.Count > 0)
{ {
table.Reason = WizardDeleteReasonEnum.InUse; table.Reason = WizardDeleteReasonEnum.InUse;

View file

@ -136,6 +136,12 @@ public static class WizardConsts
LookupQuery = lookupQuery, LookupQuery = lookupQuery,
}); });
/// <summary>
/// Query kaynagi turetilmis tablo olarak sarilir ve kolonlar bu takma adla nitelenir;
/// TableName bos kalirsa takma ad olarak sorgu metninin kendisi kullanilir.
/// </summary>
public const string QuerySourceAlias = "Q";
public static string DefaultDeleteCommand(string tableName) public static string DefaultDeleteCommand(string tableName)
{ {
return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id"; return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id";

View file

@ -88,6 +88,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
private string From { get { return SelectCommandType == SelectCommandTypeEnum.Query ? $"({SelectCommand})" : SelectCommand; } } private string From { get { return SelectCommandType == SelectCommandTypeEnum.Query ? $"({SelectCommand})" : SelectCommand; } }
private SelectCommandTypeEnum SelectCommandType { get; set; } private SelectCommandTypeEnum SelectCommandType { get; set; }
private string KeyFieldName { get; set; } private string KeyFieldName { get; set; }
private DbType KeyFieldDbSourceType { get; set; }
private List<string> JoinParts { get; set; } private List<string> JoinParts { get; set; }
private List<string> WhereParts { get; set; } private List<string> WhereParts { get; set; }
private List<string> SortParts { get; set; } private List<string> SortParts { get; set; }
@ -154,6 +155,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
SelectCommand = DefaultValueHelper.GetDefaultValue(listform.SelectCommand); SelectCommand = DefaultValueHelper.GetDefaultValue(listform.SelectCommand);
TableName = listform.TableName.IsNullOrEmpty() ? SelectCommand : listform.TableName; TableName = listform.TableName.IsNullOrEmpty() ? SelectCommand : listform.TableName;
KeyFieldName = listform.KeyFieldName; KeyFieldName = listform.KeyFieldName;
KeyFieldDbSourceType = listform.KeyFieldDbSourceType;
SelectCommandType = listform.SelectCommandType; SelectCommandType = listform.SelectCommandType;
var KeyField = new SelectField var KeyField = new SelectField
{ {
@ -221,6 +223,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
SelectCommand = null; SelectCommand = null;
TableName = null; TableName = null;
KeyFieldName = null; KeyFieldName = null;
KeyFieldDbSourceType = default;
SelectFields = []; SelectFields = [];
JoinParts = []; JoinParts = [];
WhereParts = []; WhereParts = [];
@ -296,7 +299,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
private string GetSelectString() private string GetSelectString()
{ {
return $"SELECT {string.Join(',', SelectFields.Select(c => $"{c.SelectFieldName}"))} FROM \"{From}\" AS \"{TableName}\""; return $"SELECT {string.Join(',', SelectFields.Select(c => $"{c.SelectFieldName}"))} FROM {From} AS \"{TableName}\"";
} }
private string SetJoinStringFromCustomizations(List<ListFormCustomization> listFormCustomizations) private string SetJoinStringFromCustomizations(List<ListFormCustomization> listFormCustomizations)
@ -547,9 +550,16 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
var filterParamKey = $"@{filterFieldName}{customKey}"; var filterParamKey = $"@{filterFieldName}{customKey}";
var filterParamKeyLower = filterParamKey; var filterParamKeyLower = filterParamKey;
var listFormField = listFormFields.FirstOrDefault(c => c.FieldName == filterFieldName); var listFormField = listFormFields.FirstOrDefault(c => c.FieldName == filterFieldName);
if (listFormField == null) // Anahtar alan her sorguya eklenir ama alan listesinde olmayabilir; grid satira odaklanirken
// filtreye anahtar kosulu yazar.
var isKeyField = listFormField == null && filterFieldName == KeyFieldName;
if (listFormField == null && !isKeyField)
{ {
return sqlPart; // Kosul dusurulurse etrafindaki baglac ve parantezler yetim kalir (WHERE and (...)));
// yerine parantezleri koruyan etkisiz bir kosul yazilir.
var leading = fieldName[..^fieldName.TrimStart('(').Length];
var trailing = fieldValue[fieldValue.TrimEnd(')').Length..];
return $"{leading}1 = 1{trailing}";
} }
var filterOperator = op.RemoveParentheses(); var filterOperator = op.RemoveParentheses();
@ -558,7 +568,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
DbType dbType = filterGroupInterval switch DbType dbType = filterGroupInterval switch
{ {
"year" or "quarter" or "month" or "day" or "dayOfWeek" => DbType.Int32, "year" or "quarter" or "month" or "day" or "dayOfWeek" => DbType.Int32,
_ => listFormField.SourceDbType _ => isKeyField ? KeyFieldDbSourceType : listFormField.SourceDbType
}; };
var filterValue = fieldValue.RemoveParentheses(); var filterValue = fieldValue.RemoveParentheses();
@ -689,7 +699,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
private string GetTotalCountQuery() private string GetTotalCountQuery()
{ {
var sql = $"SELECT COUNT(\"{TableName}\".\"{KeyFieldName}\") FROM \"{From}\" AS \"{TableName}\""; var sql = $"SELECT COUNT(\"{TableName}\".\"{KeyFieldName}\") FROM {From} AS \"{TableName}\"";
sql += GetJoinString(); sql += GetJoinString();
sql += GetWhereString(); sql += GetWhereString();
@ -698,7 +708,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
private string GetSummaryQuery(string fieldName, string summaryType) private string GetSummaryQuery(string fieldName, string summaryType)
{ {
var sql = $"SELECT {summaryType}({fieldName}) FROM \"{From}\" AS \"{TableName}\""; var sql = $"SELECT {summaryType}({fieldName}) FROM {From} AS \"{TableName}\"";
sql += GetJoinString(); sql += GetJoinString();
sql += GetWhereString(); sql += GetWhereString();
@ -767,7 +777,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
sqlSub.Append(string.Join(',', GroupTuples.Select((a) => $"{a.Field} AS \"{a.SelectExpr}\""))); sqlSub.Append(string.Join(',', GroupTuples.Select((a) => $"{a.Field} AS \"{a.SelectExpr}\"")));
sqlSub.Append(string.Join("", GroupSummaryTuples.Select((a) => $",{a.SummaryType}({a.Field}) AS \"{a.SelectExpr}\""))); sqlSub.Append(string.Join("", GroupSummaryTuples.Select((a) => $",{a.SummaryType}({a.Field}) AS \"{a.SelectExpr}\"")));
sqlSub.Append(",COUNT(1) AS \"Summary_Count\""); sqlSub.Append(",COUNT(1) AS \"Summary_Count\"");
sqlSub.Append($" FROM \"{From}\" AS \"{TableName}\""); sqlSub.Append($" FROM {From} AS \"{TableName}\"");
sqlSub.Append(GetJoinString()); sqlSub.Append(GetJoinString());
sqlSub.Append(GetWhereString()); sqlSub.Append(GetWhereString());
sqlSub.Append($" GROUP BY {string.Join(',', GroupTuples.Select((a) => a.Field))}"); sqlSub.Append($" GROUP BY {string.Join(',', GroupTuples.Select((a) => a.Field))}");

View file

@ -171,7 +171,14 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
sorulmadan **en az 3 kartlık** `Widgets` ile gelir; kartlar tablonun durum / boolean bayrak / sorulmadan **en az 3 kartlık** `Widgets` ile gelir; kartlar tablonun durum / boolean bayrak /
tarih / tutar kolonlarından seçilir, başlıklar dil anahtarıdır (`App.Widget.*` önce). Kiracı tarih / tutar kolonlarından seçilir, başlıklar dil anahtarıdır (`App.Widget.*` önce). Kiracı
tablosunda her kart `"TenantId" = '@TENANTID'` taşır; `IsDeleted` koşulu yazılmaz. Form/Chart, tablosunda her kart `"TenantId" = '@TENANTID'` taşır; `IsDeleted` koşulu yazılmaz. Form/Chart,
menüsüz alt form ve basit tanım listeleri istisnadır. Ayrıntı: `lowcode.instructions.md` §4.6.1. alt form ve basit tanım listeleri istisnadır. **Alt form oluşturulurken widget oluşturulmaz**:
başka bir ekranın `SubForms` sekmesi olarak üretilen her ekran — menüsüz ya da kendi menüsü olan —
`Widgets: []` ile deploy edilir. Deploy menüsüz (`CreateMenu: false`) wizard'ın `Widgets`'ını
ayrıca düşürür; ekran alt form sekmesinde açıldığında şerit de çizilmez
(`useListFormWidgets(…, isSubForm)`) — kart sorgusu ana kayda göre süzülmez. Ayrıntı:
`lowcode.instructions.md` §4.6.1.
Alt form ekranının menü/yetki/düzenleme kuralları (menüsüz alt formun yetkisi ana ekranın altında,
`Form` sekmesi tek kayıt + `AllowAdding`/`AllowUpdating` açık): `lowcode.instructions.md` §4.6.
### Cross-cutting: touching one thing means touching these too ### Cross-cutting: touching one thing means touching these too

File diff suppressed because one or more lines are too long

View file

@ -64362,6 +64362,48 @@
"Key": "App.ListForm.WizardDevRequestExport", "Key": "App.ListForm.WizardDevRequestExport",
"Value": "Talebin tüm wizard'larını dışa aktar ({0})" "Value": "Talebin tüm wizard'larını dışa aktar ({0})"
}, },
{
"Id": "798fdbec-b481-5961-bfa6-645a9f437cb8",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListForm.WizardDevRequestDelete",
"Value": "Delete all wizards of this request ({0})"
},
{
"Id": "b1f17de2-32d4-5bf4-aaea-4fc913776591",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListForm.WizardDevRequestDelete",
"Value": "Talebin tüm wizard'larını sil ({0})"
},
{
"Id": "e2ae233f-1d49-510f-a562-5a59d450bb6e",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListForm.WizardDevRequestDeleteConfirm",
"Value": "All wizards of this request are deleted in the order below; sub-forms go before the screen that carries them."
},
{
"Id": "a8a4e5ca-3e48-57ac-b526-c113bf206a6c",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListForm.WizardDevRequestDeleteConfirm",
"Value": "Talebin tüm wizard'ları aşağıdaki sırayla silinir; alt formlar onları taşıyan ekrandan önce gider."
},
{
"Id": "4bebcab5-7910-57bb-8582-d31c4f5d205e",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListForm.WizardDeleteOrder",
"Value": "Deletion order"
},
{
"Id": "3c9dafd9-a74d-5467-870b-e5373fa6d4b5",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListForm.WizardDeleteOrder",
"Value": "Silme sırası"
},
{ {
"Id": "0f8facf0-6218-5250-8038-4775b770f7e8", "Id": "0f8facf0-6218-5250-8038-4775b770f7e8",
"CultureName": "en", "CultureName": "en",
@ -70451,6 +70493,62 @@
"ResourceName": "Platform", "ResourceName": "Platform",
"Key": "App.Listform.ListformField.VisitReason", "Key": "App.Listform.ListformField.VisitReason",
"Value": "Ziyaret Sebebi" "Value": "Ziyaret Sebebi"
},
{
"Id": "90b68f83-f0f5-5fb3-aaef-b62cc24d716e",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.JobPositionName",
"Value": "Position"
},
{
"Id": "5712290c-c9f4-56e0-a181-0a49eca9d4d6",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.JobPositionName",
"Value": "Görevi"
},
{
"Id": "7eca5f6d-e78b-5339-ba0d-cfc822f931c6",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.BloodType",
"Value": "Blood Type"
},
{
"Id": "59b7c58e-3475-5e34-99dd-1c41f6fac0d4",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.BloodType",
"Value": "Kan Grubu"
},
{
"Id": "88535017-2c1c-54ed-aba5-2ba1c977e8fc",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.ShiftWorkFit",
"Value": "Fit for Shift Work"
},
{
"Id": "e509bacb-2529-5256-8bde-480d34679839",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.ShiftWorkFit",
"Value": "Vardiyalı Çalışabilir"
},
{
"Id": "d946cf5c-9dec-5143-92be-7b126520c28d",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.NightWorkFit",
"Value": "Fit for Night Work"
},
{
"Id": "96ccf430-d007-52a1-ae5d-378f70724355",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.Listform.ListformField.NightWorkFit",
"Value": "Gece Çalışabilir"
} }
] ]
} }

View file

@ -239,6 +239,19 @@ export interface WizardDeleteAnalysisDto {
tables: WizardDeleteTableDto[] tables: WizardDeleteTableDto[]
} }
/** Talebin wizard'ları silinme sırasıyla ve onayda sorulacak tablolar. */
export interface WizardDeleteDevRequestAnalysisDto {
devRequestId: string
title: string
files: { fileName: string; wizardName: string }[]
tables: WizardDeleteTableDto[]
}
export interface WizardDeleteDevRequestInput {
devRequestId: string
dropTables: string[]
}
export interface WizardDeleteInput { export interface WizardDeleteInput {
fileName: string fileName: string
dropTables: string[] dropTables: string[]

View file

@ -2,6 +2,8 @@ import {
ListFormWizardDto, ListFormWizardDto,
WizardExportAnalysisDto, WizardExportAnalysisDto,
WizardDeleteAnalysisDto, WizardDeleteAnalysisDto,
WizardDeleteDevRequestAnalysisDto,
WizardDeleteDevRequestInput,
WizardDeleteInput, WizardDeleteInput,
WizardDeleteResultDto, WizardDeleteResultDto,
WizardFileInfoDto, WizardFileInfoDto,
@ -55,6 +57,22 @@ export const deleteWizardFile = (input: WizardDeleteInput) =>
data: input, data: input,
}) })
/** Talebin wizard'larinin silinme sirasi ve secilebilen tablolari; hicbir seye dokunmaz. */
export const getWizardDevRequestDeleteAnalysis = (devRequestId: string) =>
apiService.fetchData<WizardDeleteDevRequestAnalysisDto>({
method: 'GET',
url: `/api/app/list-form-wizard/delete-dev-request-analysis`,
params: { devRequestId },
})
/** Talebin butun wizard'larini bagimlilik sirasiyla siler; rapor tek wizard silmeyle aynidir. */
export const deleteWizardDevRequest = (input: WizardDeleteDevRequestInput) =>
apiService.fetchData<WizardDeleteResultDto, WizardDeleteDevRequestInput>({
method: 'POST',
url: `/api/app/list-form-wizard/delete-dev-request`,
data: input,
})
/** /**
* Indirmeden once arsivin tam icerigini ve cozulemeyen bagimliliklarini getirir; export ile * Indirmeden once arsivin tam icerigini ve cozulemeyen bagimliliklarini getirir; export ile
* ayni toplamayi kullanir, dolayisiyla onizlemedeki liste indirilen zip ile birebir aynidir. * ayni toplamayi kullanir, dolayisiyla onizlemedeki liste indirilen zip ile birebir aynidir.

View file

@ -38,6 +38,8 @@ import {
exportWizardDevRequest, exportWizardDevRequest,
exportWizardFile, exportWizardFile,
getWizardDeleteAnalysis, getWizardDeleteAnalysis,
getWizardDevRequestDeleteAnalysis,
deleteWizardDevRequest,
getWizardDevRequestExportAnalysis, getWizardDevRequestExportAnalysis,
getWizardExportAnalysis, getWizardExportAnalysis,
getWizardFiles, getWizardFiles,
@ -73,6 +75,10 @@ interface ConfirmState {
tables: WizardDeleteTableDto[] | null tables: WizardDeleteTableDto[] | null
/** Onayda seçilen, düşürülecek tablolar. */ /** Onayda seçilen, düşürülecek tablolar. */
dropTables: string[] dropTables: string[]
/** Talebin bütün wizard'ları silinirken dolu; tek wizard silmede yoktur. */
devRequestId?: string
/** Toplu silmenin sırası; analiz gelene kadar null. */
order?: { fileName: string; wizardName: string }[] | null
} }
const DELETE_STATUS_ORDER = [ const DELETE_STATUS_ORDER = [
@ -108,13 +114,21 @@ interface WizardTreeNode {
/** /**
* Bağlı wizard'ları üst ekranın altında toplar. Kök, hiçbir wizard'ın bağımlılığı olmayan * Bağlı wizard'ları üst ekranın altında toplar. Kök, hiçbir wizard'ın bağımlılığı olmayan
* dosyadır; birden fazla ekranın kullandığı bir alt form her birinin altında görünür. Döngüdeki * dosyadır; birden fazla ekranın kullandığı bir alt form her birinin altında görünür. Kendi
* dosyaların kökü olmadığı için hiç çizilmeyenler sonda kök olarak eklenir. * menüsü olan ekran başka bir ekranın sekmesinde açılsa da kendi başına bir ekrandır; kök kalır
* (export kapanışı `dependencies` üzerinden onu yine taşır). Döngüdeki dosyaların kökü olmadığı
* için hiç çizilmeyenler sonda kök olarak eklenir.
*/ */
const buildWizardTree = (files: WizardFileInfoDto[]): WizardTreeNode[] => { const buildWizardTree = (files: WizardFileInfoDto[]): WizardTreeNode[] => {
const byName = new Map(files.map((f) => [f.fileName.toLowerCase(), f])) const byName = new Map(files.map((f) => [f.fileName.toLowerCase(), f]))
const isNestable = (f: WizardFileInfoDto) => (f.menuPath ?? []).length === 0
const childNames = new Set( const childNames = new Set(
files.flatMap((f) => (f.dependencies ?? []).map((d) => d.toLowerCase())), files.flatMap((f) =>
(f.dependencies ?? [])
.map((d) => byName.get(d.toLowerCase()))
.filter((c): c is WizardFileInfoDto => !!c && isNestable(c))
.map((c) => c.fileName.toLowerCase()),
),
) )
const rendered = new Set<string>() const rendered = new Set<string>()
@ -124,7 +138,10 @@ const buildWizardTree = (files: WizardFileInfoDto[]): WizardTreeNode[] => {
const nextPath = new Set(path).add(key) const nextPath = new Set(path).add(key)
const children = (file.dependencies ?? []) const children = (file.dependencies ?? [])
.map((d) => byName.get(d.toLowerCase())) .map((d) => byName.get(d.toLowerCase()))
.filter((c): c is WizardFileInfoDto => !!c && !nextPath.has(c.fileName.toLowerCase())) .filter(
(c): c is WizardFileInfoDto =>
!!c && isNestable(c) && !nextPath.has(c.fileName.toLowerCase()),
)
.map((c) => build(c, nextPath)) .map((c) => build(c, nextPath))
return { file, children } return { file, children }
} }
@ -155,6 +172,10 @@ interface WizardTreeRow {
/** Kökten gelen yol; ortak alt form her üst ekranın altında ayrı açılıp kapanabilsin diye. */ /** Kökten gelen yol; ortak alt form her üst ekranın altında ayrı açılıp kapanabilsin diye. */
key: string key: string
hasChildren: boolean hasChildren: boolean
/** Satır kardeşlerinin sonuncusu mu; bağlantı çizgisi burada biter (└). */
isLast: boolean
/** Her üst seviye için, o seviyedeki atanın altında kardeş kalıp kalmadığı (│ sürer mi). */
guides: boolean[]
} }
interface WizardDevRequestGroup { interface WizardDevRequestGroup {
@ -170,15 +191,32 @@ const flattenWizardTree = (
collapsed: Set<string>, collapsed: Set<string>,
depth = 0, depth = 0,
prefix = '', prefix = '',
guides: boolean[] = [],
): WizardTreeRow[] => ): WizardTreeRow[] =>
nodes.flatMap((node) => { nodes.flatMap((node, index) => {
const key = `${prefix}${node.file.fileName}` const key = `${prefix}${node.file.fileName}`
const row = { file: node.file, depth, key, hasChildren: node.children.length > 0 } const isLast = index === nodes.length - 1
const row = {
file: node.file,
depth,
key,
hasChildren: node.children.length > 0,
isLast,
guides,
}
// Kök satırın altından çizgi inmez; çocuklar kendi kolunu kökün aç/kapa düğmesinden çizer.
const childGuides = depth === 0 ? [] : [...guides, !isLast]
return collapsed.has(key) return collapsed.has(key)
? [row] ? [row]
: [row, ...flattenWizardTree(node.children, collapsed, depth + 1, `${key}/`)] : [row, ...flattenWizardTree(node.children, collapsed, depth + 1, `${key}/`, childGuides)]
}) })
/** Bir ağaç seviyesinin genişliği: aç/kapa düğmesi (14px) + satırdaki boşluk (8px). */
const TREE_INDENT = 22
/** Dikey çizginin seviye içindeki yeri; aç/kapa düğmesinin ortasına denk gelir. */
const TREE_LINE_OFFSET = 15
const TREE_LINE = 'absolute border-gray-300 dark:border-gray-600'
const WizardFileManager = () => { const WizardFileManager = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { translate } = useLocalization() const { translate } = useLocalization()
@ -400,6 +438,33 @@ const WizardFileManager = () => {
} }
} }
/**
* Talebin bütün wizard'larının silme onayı. Sıra ve tablolar sunucudan gelir: tablo kullanımı
* talebin kendi ekranlarını saymaz, hepsi aynı işlemde silinir.
*/
const openDevRequestDeleteConfirm = async (f: WizardFileInfoDto) => {
const devRequestId = f.devRequestId!
const key = `devRequest:${devRequestId}`
setConfirm({
fileName: key,
wizardName: f.devRequestTitle || devRequestId,
tables: null,
dropTables: [],
devRequestId,
order: null,
})
try {
const res = await getWizardDevRequestDeleteAnalysis(devRequestId)
setConfirm((prev) =>
prev?.fileName === key
? { ...prev, tables: res.data.tables ?? [], order: res.data.files ?? [] }
: prev,
)
} catch {
setConfirm((prev) => (prev?.fileName === key ? { ...prev, tables: [], order: [] } : prev))
}
}
const toggleDropTable = (tableName: string, checked: boolean) => const toggleDropTable = (tableName: string, checked: boolean) =>
setConfirm((prev) => setConfirm((prev) =>
prev prev
@ -417,10 +482,15 @@ const WizardFileManager = () => {
setDeletingFile(confirm.fileName) setDeletingFile(confirm.fileName)
setConfirm(null) setConfirm(null)
try { try {
const res = await deleteWizardFile({ const res = confirm.devRequestId
fileName: confirm.fileName, ? await deleteWizardDevRequest({
dropTables: confirm.dropTables, devRequestId: confirm.devRequestId,
}) dropTables: confirm.dropTables,
})
: await deleteWizardFile({
fileName: confirm.fileName,
dropTables: confirm.dropTables,
})
setDeleteReport(res.data) setDeleteReport(res.data)
toast.push( toast.push(
<Notification type="success" duration={3000}> <Notification type="success" duration={3000}>
@ -682,6 +752,20 @@ const WizardFileManager = () => {
onClick={() => handleExport(job)} onClick={() => handleExport(job)}
/> />
)} )}
{canDelete && (
<Button
type="button"
variant="plain"
shape="none"
title={translate('::App.ListForm.WizardDevRequestDelete', {
0: devRequestCounts.get(f.devRequestId) ?? 1,
})}
loading={deletingFile === `devRequest:${f.devRequestId}`}
icon={<FaTrash className="h-3.5 w-3.5" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red} shrink-0`}
onClick={() => openDevRequestDeleteConfirm(f)}
/>
)}
</span> </span>
) )
} }
@ -689,7 +773,40 @@ const WizardFileManager = () => {
const componentName = (f: WizardFileInfoDto) => const componentName = (f: WizardFileInfoDto) =>
f.componentKind === WizardComponentKindEnum.Custom ? f.customComponentName : f.listFormCode f.componentKind === WizardComponentKindEnum.Custom ? f.customComponentName : f.listFormCode
/** Alt satırlardaki bağlantı işareti; derinlik kadar içeri itilir. */ /**
* Tablo satırının ağaç çizgileri: üst seviyelerde kardeşi kalan atalar için dikey çizgi (│),
* satırın kendi seviyesinde bağlantı (├ ya da son kardeşte └). Kök satırda boş kalır ama
* satırdaki boşluğu korur; böylece her seviyenin düğmesi aynı hizada durur.
*/
const renderTreeGuides = (row: WizardTreeRow) => (
<span aria-hidden className="flex shrink-0 self-stretch">
{row.depth > 0 &&
row.guides.map((open, level) => (
<span key={level} className="relative shrink-0" style={{ width: TREE_INDENT }}>
{open && (
<span
className={`${TREE_LINE} top-0 bottom-0 border-l`}
style={{ left: TREE_LINE_OFFSET }}
/>
)}
</span>
))}
{row.depth > 0 && (
<span className="relative shrink-0" style={{ width: TREE_INDENT }}>
<span
className={`${TREE_LINE} top-0 border-l ${row.isLast ? 'h-1/2' : 'bottom-0'}`}
style={{ left: TREE_LINE_OFFSET }}
/>
<span
className={`${TREE_LINE} top-1/2 border-t`}
style={{ left: TREE_LINE_OFFSET, right: -8 }}
/>
</span>
)}
</span>
)
/** Kart içindeki bağlı satırların işareti; derinlik kadar içeri itilir. */
const renderDepthMarker = (depth: number) => const renderDepthMarker = (depth: number) =>
depth > 0 && ( depth > 0 && (
<FaLevelUpAlt <FaLevelUpAlt
@ -1014,9 +1131,10 @@ const WizardFileManager = () => {
: 'bg-gray-50/50 dark:bg-gray-800/40', : 'bg-gray-50/50 dark:bg-gray-800/40',
)} )}
> >
<td className="py-2 pl-8 pr-4"> {/* Çizgiler satır boyunca kesintisiz insin diye hücre dolgusuz, yükseklik içte. */}
<div className="flex items-center gap-2 min-w-0"> <td className="h-px py-0 pl-8 pr-4">
{renderDepthMarker(depth)} <div className="flex h-full min-h-[2.75rem] min-w-0 items-center gap-2">
{renderTreeGuides(row)}
{renderGroupToggle(row)} {renderGroupToggle(row)}
<Icon className="text-indigo-400 shrink-0 text-lg" /> <Icon className="text-indigo-400 shrink-0 text-lg" />
<span <span
@ -1090,10 +1208,26 @@ const WizardFileManager = () => {
</p> </p>
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-gray-500 mt-1">
<strong>{confirm?.wizardName}</strong>{' '} <strong>{confirm?.wizardName}</strong>{' '}
{translate('::App.ListForm.WizardFileDeleteConfirm')} {translate(
confirm?.devRequestId
? '::App.ListForm.WizardDevRequestDeleteConfirm'
: '::App.ListForm.WizardFileDeleteConfirm',
)}
</p> </p>
</div> </div>
</div> </div>
{!!confirm?.order?.length && (
<div className="mt-4 rounded border border-gray-200 p-3 dark:border-gray-700">
<p className="text-sm font-medium text-gray-800 dark:text-gray-200">
{translate('::App.ListForm.WizardDeleteOrder')}
</p>
<ol className="mt-1 list-decimal pl-5 text-xs text-gray-600 dark:text-gray-300">
{confirm.order.map((item) => (
<li key={item.fileName}>{item.wizardName}</li>
))}
</ol>
</div>
)}
{confirm?.tables === null && ( {confirm?.tables === null && (
<p className="mt-4 text-xs text-gray-500"> <p className="mt-4 text-xs text-gray-500">
{translate('::App.ListForm.WizardDeleteTablesLoading')} {translate('::App.ListForm.WizardDeleteTablesLoading')}

View file

@ -373,7 +373,7 @@ const CardView = (props: CardViewProps) => {
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [formData, setFormData] = useState<Record<string, any>>() const [formData, setFormData] = useState<Record<string, any>>()
const widgetGroupRef = useRef<HTMLDivElement>(null) const widgetGroupRef = useRef<HTMLDivElement>(null)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto) const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
const formDataRef = useRef<Record<string, any> | undefined>(undefined) const formDataRef = useRef<Record<string, any> | undefined>(undefined)
useEffect(() => { useEffect(() => {
formDataRef.current = formData formDataRef.current = formData

View file

@ -102,7 +102,7 @@ const GanttView = (props: GanttViewProps) => {
const pendingStateRef = useRef<Record<string, any> | undefined>(undefined) const pendingStateRef = useRef<Record<string, any> | undefined>(undefined)
const loadedStateKeyRef = useRef('') const loadedStateKeyRef = useRef('')
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto) const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets) const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto) useListFormCustomSources(gridDto)

View file

@ -128,7 +128,7 @@ const Grid = (props: GridProps) => {
entityId: string entityId: string
} | null>(null) } | null>(null)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto) const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets) const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto) useListFormCustomSources(gridDto)

View file

@ -349,8 +349,8 @@ const Pivot = (props: PivotProps) => {
return ( return (
<> <>
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}> <div ref={widgetGroupRef} className={!isSubForm && (gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} /> <WidgetGroup widgetGroups={isSubForm ? [] : (gridDto?.widgets ?? [])} />
</div> </div>
<Container className={DX_CLASSNAMES}> <Container className={DX_CLASSNAMES}>

View file

@ -73,7 +73,7 @@ const SchedulerView = (props: SchedulerViewProps) => {
const [userAvatarUrls, setUserAvatarUrls] = useState<Record<string, string>>({}) const [userAvatarUrls, setUserAvatarUrls] = useState<Record<string, string>>({})
const layout = layoutTypes.scheduler || 'scheduler' const layout = layoutTypes.scheduler || 'scheduler'
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto) const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets) const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto) useListFormCustomSources(gridDto)

View file

@ -118,7 +118,7 @@ const Tree = (props: TreeProps) => {
entityId: string entityId: string
} | null>(null) } | null>(null)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto) const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets) const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto) useListFormCustomSources(gridDto)

View file

@ -54,14 +54,24 @@ export const useListFormGridDto = (listFormCode: string, externalGridDto?: GridD
* widget'ları yeniden çeker. GridDto'nun kendisi değiştirilmez: yeni referans kolonları * widget'ları yeniden çeker. GridDto'nun kendisi değiştirilmez: yeni referans kolonları
* ve veri kaynağını baştan kurar, görünüm durumunu (sayfa, seçim, açık satır) sıfırlardı. * ve veri kaynağını baştan kurar, görünüm durumunu (sayfa, seçim, açık satır) sıfırlardı.
*/ */
export const useListFormWidgets = (listFormCode: string, gridDto?: GridDto) => { // Alt form sekmesinde widget çizilmez: widget sorgusu ana kaydın ilişkisiyle süzülmez, kartlar
// sekmedeki satırlarla değil bütün tabloyla ilgili sayılar gösterirdi.
export const useListFormWidgets = (
listFormCode: string,
gridDto?: GridDto,
isSubForm?: boolean,
) => {
const [state, setState] = useState<{ source?: GridDto; widgets: GridDto['widgets'] }>({ const [state, setState] = useState<{ source?: GridDto; widgets: GridDto['widgets'] }>({
widgets: [], widgets: [],
}) })
const requestIdRef = useRef(0) const requestIdRef = useRef(0)
const widgets = state.source === gridDto ? state.widgets : (gridDto?.widgets ?? []) const widgets = isSubForm
const hasWidgets = (gridDto?.widgets?.length ?? 0) > 0 ? []
: state.source === gridDto
? state.widgets
: (gridDto?.widgets ?? [])
const hasWidgets = !isSubForm && (gridDto?.widgets?.length ?? 0) > 0
const refreshWidgets = useCallback(async () => { const refreshWidgets = useCallback(async () => {
if (!listFormCode || !gridDto || !hasWidgets) return if (!listFormCode || !gridDto || !hasWidgets) return