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`) |
|
| `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`)
|
||||||
|
|
||||||
|
|
|
||||||
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 |
|
| 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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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))}");
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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[]
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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')}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue