WizardFileManager güncellemesi
This commit is contained in:
parent
6081131f95
commit
0bc44e726c
24 changed files with 757 additions and 74 deletions
|
|
@ -103,8 +103,8 @@ ve çoğu isteğin cevabı aslında oradadır:
|
|||
| `DataSourceCode` | `"Default"` | Hangi bağlantıdan okunacağı (`Sas_H_DataSource`) |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `KeyFieldName` · `KeyFieldDbSourceType` | — · `16` String | Anahtar kolon ve `System.Data.DbType` karşılığı |
|
||||
| `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ığı. `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 |
|
||||
| `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) |
|
||||
|
|
@ -375,7 +375,7 @@ Wizard varsayılan olarak `DefaultFieldPermissionJson(permissionName)` üretir.
|
|||
|
||||
| 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ı |
|
||||
| `Code` | Hedef `ListFormCode` |
|
||||
| `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
|
||||
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.
|
||||
- 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`)
|
||||
|
||||
|
|
|
|||
132
.github/instructions/lowcode.instructions.md
vendored
132
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -259,7 +259,7 @@ bildirilir:
|
|||
| 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 |
|
||||
| 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ç
|
||||
ü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 |
|
||||
| `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz |
|
||||
| `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` |
|
||||
| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). List ekranında `Widgets` dolu gelir — en az 3 kart, kiracı tablosunda `"TenantId" = '@TENANTID'` (§4.6.1); kartlardaki her dil anahtarı var olmalı ya da `Language.Texts`'te EN + TR verilmeli, yoksa deploy reddeder. `MenuOrder` yazılmaz — sunucu yeni ekranı kardeşlerinin sonuna koyar, güncellemede yerini korur. 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) |
|
||||
| `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 |
|
||||
|
|
@ -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ü
|
||||
`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.
|
||||
- **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,
|
||||
`Analysis`'e geri alınmaz, kısmi ekran üretilmez. Üretilebilirlik analizde doğrulanmıştır
|
||||
(§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.
|
||||
- Talebin açıkça yazdığı şey (menü yolu, alan, kapsam) **girdidir**: değiştirilmez, daraltılmaz,
|
||||
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
|
||||
`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
|
||||
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).
|
||||
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
|
||||
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.
|
||||
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
|
||||
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,
|
||||
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
|
||||
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.
|
||||
**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
|
||||
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
|
||||
(`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
|
||||
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
|
||||
`dxImageUpload`'dır (madde 5).
|
||||
8. **Kararlar** — talepten okunamayıp analizde verilen her karar, tek satır gerekçesiyle (§0.6);
|
||||
`dxImageUpload`'dır (madde 5). Widget kartları yalnızca ana ekrana yazılır; alt form olarak
|
||||
ü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.
|
||||
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.
|
||||
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
|
||||
eklenene kadar `Ready to Start`'a alınmaz.
|
||||
|
||||
|
|
@ -810,7 +837,13 @@ yol elle kurulmaz.
|
|||
**`ComponentKind`** (`WizardComponentKindEnum`) — `0` List · `1` Custom
|
||||
|
||||
**`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
|
||||
|
||||
|
|
@ -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.
|
||||
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
|
||||
|
||||
**`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.
|
||||
|
||||
**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`,
|
||||
`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:
|
||||
|
|
@ -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,
|
||||
`yellow`/`orange` bekleyen, `purple` tutar/özet. İkon `react-icons/fa` adıdır (`FaCheckCircle`,
|
||||
`FaTimesCircle`, `FaCalendarDay`, `FaEye`, `FaMoneyBillWave`…).
|
||||
- **İstisna — widget eklenmez:** Form ve Chart ekranları; alt form olarak kullanılan menüsüz
|
||||
ekranlar (`CreateMenu: false`, beğeni/yorum/seçenek gibi detay tabloları); `IsActive` de
|
||||
- **İstisna — widget eklenmez:** Form ve Chart ekranları; **alt form olarak üretilen her ekran** —
|
||||
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,
|
||||
ş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.
|
||||
- **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
|
||||
(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ı |
|
||||
| `SelectCommandType` + `SelectCommand` | Kaynak türü ve kaynağın kendisi |
|
||||
| `TableName` | Tablo adı/alias — yazma komutlarının hedefi |
|
||||
| `KeyFieldName` + `KeyFieldDbSourceType` | Anahtar |
|
||||
| `TableName` | Tablo adı/alias — yazma komutlarının hedefi; `4` Query'de zorunlu alias (`"Q"`) |
|
||||
| `KeyFieldName` + `KeyFieldDbSourceType` | Anahtar; `4` Query'de ikisi de zorunlu |
|
||||
| `SelectFieldsDefaultValueJson` | Select'e geçirilen varsayılan parametreler |
|
||||
| `DefaultFilter` | Her sorgunun sonuna eklenen WHERE (soft delete filtresi buradadır) |
|
||||
| `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
|
||||
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).
|
||||
- [ ] 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
|
||||
`"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
|
||||
|
|
|
|||
|
|
@ -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`). |
|
||||
| `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). |
|
||||
| `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ı. |
|
||||
| `ExtraFilterJson` | Ekrana özel ek filtre araç çubuğu. |
|
||||
| `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}`. |
|
||||
| **Visual Designer** | `/admin/developerkit/components/design/:id` | Custom Component listesindeki **Design** butonundan açılır; sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). |
|
||||
| **ListForm** | `/admin/list/App.Listforms.Listform` | Var olan ekranların ham tanım listesi; buradan `/admin/listform/edit/{kod}` sekmeli editörüne geçilir. |
|
||||
| **Wizard Manager** | `/admin/listform/wizardManager` | Üretilmiş wizard seed dosyalarının listesi/kartı, düzenleme, silme, DB migrate, export/import. 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
|
||||
> [`.github/instructions/lowcode.instructions.md`](.github/instructions/lowcode.instructions.md)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ public interface IListFormWizardAppService
|
|||
/// <summary>Silme onayinda sorulacak tablolari ve tabloyla birlikte gidecek seed dosyalarini dondurur.</summary>
|
||||
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>
|
||||
Task<List<WizardPermissionGroupDto>> GetPermissionGroups();
|
||||
|
||||
|
|
|
|||
|
|
@ -253,10 +253,12 @@ public class ListFormWizardDto
|
|||
IsOrganizationUnit = IsOrganizationUnit,
|
||||
SelectCommandType = SelectCommandType,
|
||||
SelectCommand = SelectCommand,
|
||||
TableName = SelectCommandType == SelectCommandTypeEnum.Query ? WizardConsts.QuerySourceAlias : null,
|
||||
KeyFieldName = KeyFieldName,
|
||||
KeyFieldDbSourceType = KeyFieldDbSourceType,
|
||||
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,
|
||||
SeedSyncInsert = SeedData,
|
||||
SeedSyncUpdate = SeedData,
|
||||
|
|
|
|||
|
|
@ -532,6 +532,31 @@ public class WizardDeleteInput
|
|||
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>
|
||||
public class WizardFileInfoDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -125,6 +125,12 @@ public class ListFormWizardAppService(
|
|||
// Ad ve kod catismalari, dosyalar yazilmaya baslamadan once reddedilir; aksi halde yarim
|
||||
// kalan bir deploy mevcut menu/permission satirlarini bozabilir.
|
||||
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 ValidateWidgetLanguageKeysAsync(input, rows);
|
||||
|
||||
|
|
@ -230,7 +236,9 @@ public class ListFormWizardAppService(
|
|||
|
||||
// Ekranin sozlesmesi tek fabrikadan uretilir; migrate ekrani bu dosyadan kurar.
|
||||
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");
|
||||
ValidateImageColumnLengths(input, tableColumns);
|
||||
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>
|
||||
/// Bagli wizard'lari (alt formlar / component bagimliliklari) olan bir wizard silinemez; once
|
||||
/// bagli olanlar silinir. Kural listedeki gruplamayla ayni kaynaktandir
|
||||
|
|
@ -1060,7 +1208,11 @@ public class ListFormWizardAppService(
|
|||
/// satirlari cikarilir. Veritabani adimi basarisizsa dosyalara dokunulmaz.</item>
|
||||
/// </list>
|
||||
/// </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);
|
||||
|
||||
|
|
@ -1075,7 +1227,7 @@ public class ListFormWizardAppService(
|
|||
|
||||
// Tablolar ekran silinmeden once analiz edilir: kullanim kontrolu ekranin kendisini disarida
|
||||
// birakir ve baglanti, veri kaynagi kaydi temizlenmeden once cozulur.
|
||||
var tables = seed != null
|
||||
var tables = seed != null && batchListForms == null
|
||||
? await _tableCleaner.AnalyzeAsync(seed)
|
||||
: [];
|
||||
var connections = await _tableCleaner.ResolveConnectionsAsync(
|
||||
|
|
@ -1091,7 +1243,7 @@ public class ListFormWizardAppService(
|
|||
List<WizardDeleteLogItemDto> items;
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
private async Task<Dictionary<string, int?>> GetTableColumnsAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -64,7 +64,11 @@ public class WizardRecordCleaner(
|
|||
/// <summary>Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.</summary>
|
||||
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 wizard = seed.Wizard;
|
||||
|
|
@ -72,11 +76,11 @@ public class WizardRecordCleaner(
|
|||
|
||||
// Custom yolunda ListForm uretilmez; ayni kodu tasiyan bir List wizard'inin ekrani silinmesin.
|
||||
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.
|
||||
foreach (var listFormCode in seed.Pending?.ListFormCodes ?? [])
|
||||
await CleanListFormAsync(listFormCode, report);
|
||||
await CleanListFormAsync(listFormCode, report, batchListForms);
|
||||
|
||||
var deletedMenus = await CleanMenusAsync(ins.MenuCodes, report);
|
||||
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
|
||||
/// kalir, aksi halde ust ekranin sekmesi bozulurdu.
|
||||
/// </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 fields = await repoListFormField.GetListAsync(a => a.ListFormCode == listFormCode);
|
||||
|
|
@ -136,8 +140,10 @@ public class WizardRecordCleaner(
|
|||
else
|
||||
{
|
||||
var quoted = Quote(listFormCode);
|
||||
var parents = await repoListForm.GetListAsync(a =>
|
||||
a.ListFormCode != listFormCode && a.SubFormsJson != null && a.SubFormsJson.Contains(quoted));
|
||||
var parents = (await repoListForm.GetListAsync(a =>
|
||||
a.ListFormCode != listFormCode && a.SubFormsJson != null && a.SubFormsJson.Contains(quoted)))
|
||||
.Where(p => batchListForms?.Contains(p.ListFormCode, StringComparer.OrdinalIgnoreCase) != true)
|
||||
.ToList();
|
||||
|
||||
if (parents.Count > 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ public class WizardTableCleaner(
|
|||
{
|
||||
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;
|
||||
if (wizard == null || wizard.ComponentKind == WizardComponentKindEnum.Custom)
|
||||
|
|
@ -76,7 +77,10 @@ public class WizardTableCleaner(
|
|||
await ReadTableStateAsync(table);
|
||||
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)
|
||||
{
|
||||
table.Reason = WizardDeleteReasonEnum.InUse;
|
||||
|
|
|
|||
|
|
@ -136,6 +136,12 @@ public static class WizardConsts
|
|||
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)
|
||||
{
|
||||
return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id";
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
|||
private string From { get { return SelectCommandType == SelectCommandTypeEnum.Query ? $"({SelectCommand})" : SelectCommand; } }
|
||||
private SelectCommandTypeEnum SelectCommandType { get; set; }
|
||||
private string KeyFieldName { get; set; }
|
||||
private DbType KeyFieldDbSourceType { get; set; }
|
||||
private List<string> JoinParts { get; set; }
|
||||
private List<string> WhereParts { get; set; }
|
||||
private List<string> SortParts { get; set; }
|
||||
|
|
@ -154,6 +155,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
|||
SelectCommand = DefaultValueHelper.GetDefaultValue(listform.SelectCommand);
|
||||
TableName = listform.TableName.IsNullOrEmpty() ? SelectCommand : listform.TableName;
|
||||
KeyFieldName = listform.KeyFieldName;
|
||||
KeyFieldDbSourceType = listform.KeyFieldDbSourceType;
|
||||
SelectCommandType = listform.SelectCommandType;
|
||||
var KeyField = new SelectField
|
||||
{
|
||||
|
|
@ -221,6 +223,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
|||
SelectCommand = null;
|
||||
TableName = null;
|
||||
KeyFieldName = null;
|
||||
KeyFieldDbSourceType = default;
|
||||
SelectFields = [];
|
||||
JoinParts = [];
|
||||
WhereParts = [];
|
||||
|
|
@ -296,7 +299,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
|||
|
||||
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)
|
||||
|
|
@ -547,9 +550,16 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
|||
var filterParamKey = $"@{filterFieldName}{customKey}";
|
||||
var filterParamKeyLower = filterParamKey;
|
||||
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();
|
||||
|
|
@ -558,7 +568,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
|||
DbType dbType = filterGroupInterval switch
|
||||
{
|
||||
"year" or "quarter" or "month" or "day" or "dayOfWeek" => DbType.Int32,
|
||||
_ => listFormField.SourceDbType
|
||||
_ => isKeyField ? KeyFieldDbSourceType : listFormField.SourceDbType
|
||||
};
|
||||
|
||||
var filterValue = fieldValue.RemoveParentheses();
|
||||
|
|
@ -689,7 +699,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
|||
|
||||
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 += GetWhereString();
|
||||
|
||||
|
|
@ -698,7 +708,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
|||
|
||||
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 += 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("", GroupSummaryTuples.Select((a) => $",{a.SummaryType}({a.Field}) AS \"{a.SelectExpr}\"")));
|
||||
sqlSub.Append(",COUNT(1) AS \"Summary_Count\"");
|
||||
sqlSub.Append($" FROM \"{From}\" AS \"{TableName}\"");
|
||||
sqlSub.Append($" FROM {From} AS \"{TableName}\"");
|
||||
sqlSub.Append(GetJoinString());
|
||||
sqlSub.Append(GetWhereString());
|
||||
sqlSub.Append($" GROUP BY {string.Join(',', GroupTuples.Select((a) => a.Field))}");
|
||||
|
|
|
|||
|
|
@ -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 /
|
||||
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,
|
||||
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
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -64362,6 +64362,48 @@
|
|||
"Key": "App.ListForm.WizardDevRequestExport",
|
||||
"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",
|
||||
"CultureName": "en",
|
||||
|
|
@ -70451,6 +70493,62 @@
|
|||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.VisitReason",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,6 +239,19 @@ export interface WizardDeleteAnalysisDto {
|
|||
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 {
|
||||
fileName: string
|
||||
dropTables: string[]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import {
|
|||
ListFormWizardDto,
|
||||
WizardExportAnalysisDto,
|
||||
WizardDeleteAnalysisDto,
|
||||
WizardDeleteDevRequestAnalysisDto,
|
||||
WizardDeleteDevRequestInput,
|
||||
WizardDeleteInput,
|
||||
WizardDeleteResultDto,
|
||||
WizardFileInfoDto,
|
||||
|
|
@ -55,6 +57,22 @@ export const deleteWizardFile = (input: WizardDeleteInput) =>
|
|||
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
|
||||
* ayni toplamayi kullanir, dolayisiyla onizlemedeki liste indirilen zip ile birebir aynidir.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import {
|
|||
exportWizardDevRequest,
|
||||
exportWizardFile,
|
||||
getWizardDeleteAnalysis,
|
||||
getWizardDevRequestDeleteAnalysis,
|
||||
deleteWizardDevRequest,
|
||||
getWizardDevRequestExportAnalysis,
|
||||
getWizardExportAnalysis,
|
||||
getWizardFiles,
|
||||
|
|
@ -73,6 +75,10 @@ interface ConfirmState {
|
|||
tables: WizardDeleteTableDto[] | null
|
||||
/** Onayda seçilen, düşürülecek tablolar. */
|
||||
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 = [
|
||||
|
|
@ -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
|
||||
* dosyadır; birden fazla ekranın kullandığı bir alt form her birinin altında görünür. Döngüdeki
|
||||
* dosyaların kökü olmadığı için hiç çizilmeyenler sonda kök olarak eklenir.
|
||||
* dosyadır; birden fazla ekranın kullandığı bir alt form her birinin altında görünür. Kendi
|
||||
* 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 byName = new Map(files.map((f) => [f.fileName.toLowerCase(), f]))
|
||||
const isNestable = (f: WizardFileInfoDto) => (f.menuPath ?? []).length === 0
|
||||
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>()
|
||||
|
||||
|
|
@ -124,7 +138,10 @@ const buildWizardTree = (files: WizardFileInfoDto[]): WizardTreeNode[] => {
|
|||
const nextPath = new Set(path).add(key)
|
||||
const children = (file.dependencies ?? [])
|
||||
.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))
|
||||
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. */
|
||||
key: string
|
||||
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 {
|
||||
|
|
@ -170,15 +191,32 @@ const flattenWizardTree = (
|
|||
collapsed: Set<string>,
|
||||
depth = 0,
|
||||
prefix = '',
|
||||
guides: boolean[] = [],
|
||||
): WizardTreeRow[] =>
|
||||
nodes.flatMap((node) => {
|
||||
nodes.flatMap((node, index) => {
|
||||
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)
|
||||
? [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 navigate = useNavigate()
|
||||
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) =>
|
||||
setConfirm((prev) =>
|
||||
prev
|
||||
|
|
@ -417,10 +482,15 @@ const WizardFileManager = () => {
|
|||
setDeletingFile(confirm.fileName)
|
||||
setConfirm(null)
|
||||
try {
|
||||
const res = await deleteWizardFile({
|
||||
fileName: confirm.fileName,
|
||||
dropTables: confirm.dropTables,
|
||||
})
|
||||
const res = confirm.devRequestId
|
||||
? await deleteWizardDevRequest({
|
||||
devRequestId: confirm.devRequestId,
|
||||
dropTables: confirm.dropTables,
|
||||
})
|
||||
: await deleteWizardFile({
|
||||
fileName: confirm.fileName,
|
||||
dropTables: confirm.dropTables,
|
||||
})
|
||||
setDeleteReport(res.data)
|
||||
toast.push(
|
||||
<Notification type="success" duration={3000}>
|
||||
|
|
@ -682,6 +752,20 @@ const WizardFileManager = () => {
|
|||
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>
|
||||
)
|
||||
}
|
||||
|
|
@ -689,7 +773,40 @@ const WizardFileManager = () => {
|
|||
const componentName = (f: WizardFileInfoDto) =>
|
||||
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) =>
|
||||
depth > 0 && (
|
||||
<FaLevelUpAlt
|
||||
|
|
@ -1014,9 +1131,10 @@ const WizardFileManager = () => {
|
|||
: 'bg-gray-50/50 dark:bg-gray-800/40',
|
||||
)}
|
||||
>
|
||||
<td className="py-2 pl-8 pr-4">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{renderDepthMarker(depth)}
|
||||
{/* Çizgiler satır boyunca kesintisiz insin diye hücre dolgusuz, yükseklik içte. */}
|
||||
<td className="h-px py-0 pl-8 pr-4">
|
||||
<div className="flex h-full min-h-[2.75rem] min-w-0 items-center gap-2">
|
||||
{renderTreeGuides(row)}
|
||||
{renderGroupToggle(row)}
|
||||
<Icon className="text-indigo-400 shrink-0 text-lg" />
|
||||
<span
|
||||
|
|
@ -1090,10 +1208,26 @@ const WizardFileManager = () => {
|
|||
</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
<strong>{confirm?.wizardName}</strong>{' '}
|
||||
{translate('::App.ListForm.WizardFileDeleteConfirm')}
|
||||
{translate(
|
||||
confirm?.devRequestId
|
||||
? '::App.ListForm.WizardDevRequestDeleteConfirm'
|
||||
: '::App.ListForm.WizardFileDeleteConfirm',
|
||||
)}
|
||||
</p>
|
||||
</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 && (
|
||||
<p className="mt-4 text-xs text-gray-500">
|
||||
{translate('::App.ListForm.WizardDeleteTablesLoading')}
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ const CardView = (props: CardViewProps) => {
|
|||
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
|
||||
const [formData, setFormData] = useState<Record<string, any>>()
|
||||
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)
|
||||
useEffect(() => {
|
||||
formDataRef.current = formData
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ const GanttView = (props: GanttViewProps) => {
|
|||
const pendingStateRef = useRef<Record<string, any> | undefined>(undefined)
|
||||
const loadedStateKeyRef = useRef('')
|
||||
|
||||
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
|
||||
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
|
||||
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
|
||||
useListFormCustomSources(gridDto)
|
||||
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ const Grid = (props: GridProps) => {
|
|||
entityId: string
|
||||
} | null>(null)
|
||||
|
||||
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
|
||||
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
|
||||
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
|
||||
useListFormCustomSources(gridDto)
|
||||
|
||||
|
|
|
|||
|
|
@ -349,8 +349,8 @@ const Pivot = (props: PivotProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
|
||||
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
|
||||
<div ref={widgetGroupRef} className={!isSubForm && (gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
|
||||
<WidgetGroup widgetGroups={isSubForm ? [] : (gridDto?.widgets ?? [])} />
|
||||
</div>
|
||||
|
||||
<Container className={DX_CLASSNAMES}>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
|||
const [userAvatarUrls, setUserAvatarUrls] = useState<Record<string, string>>({})
|
||||
const layout = layoutTypes.scheduler || 'scheduler'
|
||||
|
||||
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
|
||||
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
|
||||
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
|
||||
useListFormCustomSources(gridDto)
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ const Tree = (props: TreeProps) => {
|
|||
entityId: string
|
||||
} | null>(null)
|
||||
|
||||
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
|
||||
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto, isSubForm)
|
||||
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
|
||||
useListFormCustomSources(gridDto)
|
||||
|
||||
|
|
|
|||
|
|
@ -54,14 +54,24 @@ export const useListFormGridDto = (listFormCode: string, externalGridDto?: GridD
|
|||
* 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ı.
|
||||
*/
|
||||
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'] }>({
|
||||
widgets: [],
|
||||
})
|
||||
const requestIdRef = useRef(0)
|
||||
|
||||
const widgets = state.source === gridDto ? state.widgets : (gridDto?.widgets ?? [])
|
||||
const hasWidgets = (gridDto?.widgets?.length ?? 0) > 0
|
||||
const widgets = isSubForm
|
||||
? []
|
||||
: state.source === gridDto
|
||||
? state.widgets
|
||||
: (gridDto?.widgets ?? [])
|
||||
const hasWidgets = !isSubForm && (gridDto?.widgets?.length ?? 0) > 0
|
||||
|
||||
const refreshWidgets = useCallback(async () => {
|
||||
if (!listFormCode || !gridDto || !hasWidgets) return
|
||||
|
|
|
|||
Loading…
Reference in a new issue