Wizard ile ListFormComponent arasında bağlantı kuruldu.

This commit is contained in:
Sedat ÖZTÜRK 2026-09-02 11:32:53 +03:00
parent 4d7aaef901
commit 3622764923
41 changed files with 14096 additions and 2416 deletions

View file

@ -100,7 +100,16 @@ Ek kurallar:
- **Katman sınırı**: Domain.Shared → Domain → Application.Contracts → Application →
HttpApi → HttpApi.Host. Ters yönde referans verilmez; `EntityFrameworkCore` yalnızca
Domain'e bakar.
Domain'e bakar. `DbMigrator` yalnızca `Application.Contracts` + `EntityFrameworkCore`
referansı taşır; **Application'a referans verilmez** — seeder'ın uygulama servislerine
ihtiyaç duyması, kodun yanlış katmanda olduğunun işaretidir.
- **Hem seeder'ın hem AppService'in ihtiyaç duyduğu dönüşüm**: ortak kod
`Application.Contracts`'a konur (DTO'nun kendi metodu olarak; yeni yardımcı sınıf
üretilmez). Dönüşümün entity'ye yazması gerekiyorsa entity'nin **yazılabilir kolon
sözleşmesi** Domain.Shared'de bir arayüz olarak tanımlanır (`IListFormArtifact`,
`IListFormFieldArtifact`), entity bu arayüzü uygular ve dönüşüm arayüz üzerinden yazar.
Böylece Contracts, Domain'i tanımadan entity doldurabilir ve aynı dönüşüm iki yerde
kopyalanmaz.
- **AppService**: `PlatformAppService`'ten türer, arayüzü Application.Contracts'ta
tanımlanır. Controller yazılmaz — ABP otomatik API üretir. Controller sadece ABP
konvansiyonunun karşılamadığı durumda (dosya indirme, harici webhook) eklenir.

View file

@ -16,8 +16,11 @@ sorusunda buraya bak.
**Ortak kurallar**
- Bütün `*Json` kolonları veritabanında **tek satır JSON string** olarak durur; wizard seed
dosyasına yazarken kaçışlanır.
- Bütün `*Json` kolonları veritabanında **tek satır JSON string** olarak durur. Wizard seed
dosyasının `ListForm`/`Fields` bölümleri ekranın DTO sözleşmesini taşıdığı için orada bu
kolonlar kaçışlanmış metin değil, tipli karşılıkları (`SchedulerOptionJson` →
`SchedulerOptionDto`) olarak yer alır; kaçışlama yalnızca `EditorOptions`, `EditorScript`,
`LookupQuery` gibi gerçekten metin olan alanlar içindir.
- Varsayılan değeri olan bir alanı tekrar yazmak zorunda değilsin; yalnızca farklı olanı yaz.
- `Accepted Values` yazan yerlerde değer birebir eşleşmelidir (büyük/küçük harf dahil).
- **Bilinmeyen anahtar sessizce düşer — tarayıcıya hiç ulaşmaz.** `GridOptionsDto` bu 47

View file

@ -53,7 +53,7 @@ configs/seeds/host/
├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise)
├── crud/{EntityName}.json ← tablonun REST uçları
├── custom/{ComponentName}.json ← Custom Component (Visual Designer dokümanı ya da kod)
└── wizard/{yyyyMMddHHmmss}_{WizardName}.json ← ekran + menü + yetki + dil anahtarları
└── wizard/{WizardName}.json ← ekran + menü + yetki + dil anahtarları
```
Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen aynıdır.
@ -65,13 +65,14 @@ Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen ayn
| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı |
| 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar |
| 3 | Custom Component (gerekiyorsa) | `custom/{Name}.json` | Wizard `Custom` yolunda bileşen adını arar |
| 4 | Wizard | `wizard/{ts}_{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir |
| 5 | Alt ekranlar (varsa) | ek `wizard/…` dosyaları | Ana wizard'dan **önceki** zaman damgasını alır |
| 4 | Wizard | `wizard/{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir |
| 5 | Alt ekranlar (varsa) | ek `wizard/…` dosyaları | Ana wizard'dan **önceki** `SeededAt` damgasını alır |
**Zaman damgası bir bağımlılık aracıdır.** `WizardDataSeeder` `wizard/` klasörünü **dosya adına
göre sıralı** işler. Bir wizard başka bir wizard'ın ürettiği menüye/ListForm'a dayanıyorsa,
bağımlı olanın zaman damgası **daha büyük** olmalıdır. Alt form olarak kullanılacak menüsüz
ekranı her zaman önce numaralandır.
**`Wizard.SeededAt` bir bağımlılık aracıdır.** Dosya adı tarih taşımaz (`{WizardName}.json`);
`WizardDataSeeder` `wizard/` klasörünü **`Wizard.SeededAt` damgasına göre sıralı** işler (eşitlikte
dosya adı). Bir wizard başka bir wizard'ın ürettiği menüye/ListForm'a dayanıyorsa, bağımlı olanın
damgası **daha büyük** olmalıdır. Alt form olarak kullanılacak menüsüz ekranı her zaman önce
damgala.
### 0.3 Yazmadan önce zorunlu keşif
@ -102,7 +103,7 @@ Uydurma. Şu üçünü mutlaka kontrol et:
| JSON alan adları | **PascalCase** (`Wizard`, `ListFormCode`, `Groups`) — seeder `PropertyNameCaseInsensitive` okur ama dosyalar PascalCase üretilir |
| Custom component dosyası | Kök alanlar PascalCase; `Props` **içindeki** designer dokümanı camelCase (`visualDesigner`, `nodes`, `sourceMode`) |
| Enum'lar | Sayı olarak yazılır (§3) |
| `GeneratedAt` | ISO-8601 UTC (`2026-08-26T10:15:00Z`) — yalnızca bilgi amaçlı. **Yalnızca `custom/` ve `crud/` dosyalarında vardır**; wizard seed dosyasında böyle bir alan yoktur (zaman damgası dosya adındadır) |
| `GeneratedAt` | ISO-8601 UTC (`2026-08-26T10:15:00Z`) — yalnızca bilgi amaçlı. **Yalnızca `custom/` ve `crud/` dosyalarında vardır**; wizard seed dosyasında karşılığı `Wizard.SeededAt` alanıdır |
| İç içe JSON | `EditorOptions`, `EditorScript`, `LookupQuery` → JSON **string** olarak kaçışlanır |
| Satır sonu | LF |
@ -466,7 +467,7 @@ Tutarlılık zorunludur; menü, route, yetki ve dil anahtarı aynı kökten tür
| Dil anahtarı (alan) | `App.Listform.ListformField.{Alan}` | `App.Listform.ListformField.OrderNo` |
| Route (List) | `/admin/list/{ListFormCode}` — statik `:listFormCode` route'u karşılar, DB'ye kayıt yazılmaz | `/admin/list/App.Mrp.Orders` |
| Route (Custom) | Bileşenin `RoutePath` değeri; route doğrudan bundan üretilir | `/admin/order-board` |
| Wizard seed dosyası | `{yyyyMMddHHmmss}_{WizardName}.json` | `20260826101500_Orders.json` |
| Wizard seed dosyası | `{WizardName}.json` | `Orders.json` |
| Custom component seed | `{Name}.json` | `OrderBoard.json` |
| CRUD endpoint seed | `{EntityName}.json` | `Mrp_T_Order.json` |
@ -561,14 +562,26 @@ Her editörün kendine özgü seçenekleri için §4.3.2.
## 4. Artefakt A — ListForm ekranı (Wizard seed dosyası)
Konum: `configs/seeds/{kapsam}/wizard/{yyyyMMddHHmmss}_{WizardName}.json`
Okuyan: `WizardDataSeeder` · Yazan: `ListFormWizardAppService`
Konum: `configs/seeds/{kapsam}/wizard/{WizardName}.json` (dosya adında tarih yoktur)
Okuyan: `WizardDataSeeder` · Yazan: `ListFormWizardAppService` ve `WizardSeedSynchronizer`
### 4.1 Dosya iskeleti
```jsonc
{
"Wizard": { /* ListFormWizardDto — 4.2 */ },
"Wizard": { // yalnızca ekrandan üretilemeyen bilgiler — 4.2
"ComponentKind": 0, "WizardName": "Tags", "SeededAt": "2026-09-01T09:03:00",
"WizardCode": "App.Prj.Tags", "CustomComponentName": "",
"PermissionGroupName": "App.Prj", "PermissionGroupDisplayNameEn": "…", "PermissionGroupDisplayNameTr": "…",
"DataSourceConnectionString": "", "ModuleId": "Prj",
"Menu": { "MenuCode": "…", "MenuOrder": 2, "MenuUrl": "…", "MenuIcon": "…",
"MenuParentCode": "…", "MenuParentIcon": "…" }, // "CreateMenu": false ise eklenir
"Language": { "LanguageTextMenuEn": "…", /* Menu/Title/Desc/MenuParent · EN + TR */ },
"Groups": [ { "Items": [ { "FieldName": "…", "CaptionName": "…",
"EnglishCaption": "…", "TurkishCaption": "…" } ] } ]
},
"ListForm": { /* GridOptionsEditDto — ekranın kendisi, ListForm detayıyla aynı sözleşme */ },
"Fields": [ /* ColumnFormatEditDto — alanlar, alan detayıyla aynı sözleşme */ ],
"IsDeletedField": true, // tabloda IsDeleted var mı (soft delete)
"IsCreatedField": true, // tabloda CreatorId var mı (audit)
"InsertedRecords": { // bu çalıştırmanın GERÇEKTEN yarattığı kayıtlar
@ -578,19 +591,50 @@ Okuyan: `WizardDataSeeder` · Yazan: `ListFormWizardAppService`
}
```
`Wizard` bölümü yalnızca ekrandan üretilemeyen bilgileri taşır (menü, yetki, dil metinleri,
bileşen yolu, alan başlıklarının dil karşılıkları); `ListForm`/`Fields` içinde karşılığı olan
hiçbir alan orada tekrar edilmez (§4.2).
**`ListForm` + `Fields` ekranın tek gerçeğidir.** Seeder ekranı bu iki bölümden kurar; `Wizard`
bölümünden yalnızca menü, yetki, dil anahtarları ve veri kaynağı üretilir. Ekran **ListForm
detayından** (liste ayarları, alan ayarları, JSON satır editörleri) düzenlendiğinde sunucu aynı
dosyanın `ListForm`/`Fields` bölümlerini ve `Wizard` bölümündeki karşılıklarını geri yazar
(`WizardSeedSynchronizer`); böylece wizard yeniden deploy edildiğinde tasarımcıdaki iş kaybolmaz.
Elle bu dosyayı yazarken `ListForm`/`Fields` bölümlerini uydurma — wizard'ı deploy et, dosyayı
sunucu üretsin.
`InsertedRecords` **silme sözleşmesidir**: wizard silindiğinde yalnızca burada listelenen
kayıtlar silinir, paylaşılan kayıtlara (var olan izin grubu, var olan üst menü) dokunulmaz.
Elle dosya yazarken buraya var olan bir kaydı koyma — silme onu da götürür.
### 4.2 `Wizard` bloğu — alan alan
`Wizard` bloğu **yalnızca ekranın sözleşmesinden üretilemeyen** bilgileri taşır: menü, yetki,
dil metinleri, bileşen yolu ve alan başlıklarının iki dilli karşılıkları. Ekrana ait olan her
şey (veri kaynağı, select komutu, anahtar alan, kırılım bayrakları, düzen/görünüm bayrakları,
düzenleme izinleri, görünüm option blokları, alt formlar, widget'lar, iş akışı, alan tanımları)
`ListForm`/`Fields` bölümlerindedir ve dosyaya **ikinci kez yazılmaz**.
Dosyada wizard alanları üç başlık altında toplanır — `Menu` (menü kaydı), `Language` (dil
metinleri), `Groups` (alan başlıklarının dil karşılıkları); geri kalanı kök seviyededir.
Gruplama ile `ListFormCode → WizardCode` ve `MenuParentModuleId → ModuleId` adlandırmaları
yalnızca **dosya biçimidir**; C#
sözleşmesi (`ListFormWizardDto`) düzdür ve dosya okunurken düzleştirilir
(`WizardSeedFileDto.FromSeedJson` ↔ `ToSeedJson`). Aşağıdaki tablolar C# alan adlarını kullanır.
Aşağıdaki tablolarda `[ListForm]` işaretli alanlar **istek girdisidir**: sihirbaz bunları POST
eder, sunucu ekranı bunlardan üretir, ama seed dosyasında `ListForm` bölümündeki karşılıkları
saklanır. Dosya okunurken bu alanlar `ListForm`/`Fields` üzerinden geri doldurulur
(`WizardSeedFileDto.FromSeedJson`), dolayısıyla wizard'ı okuyan kod tam nesneyi görür.
**Kimlik ve yol**
| Alan | Anlam |
| --- | --- |
| `ComponentKind` | `0` List (varsayılan) · `1` Custom. Dosyadaki ilk alan olmalı. |
| `WizardName` | Dosya adının ve export zip adının kaynağı. |
| `ListFormCode` | Ekranın kodu; route ve metadata bu koda göre çözülür. |
| `WizardName` | Dosya adının (`{WizardName}.json`) ve export zip adının kaynağı. |
| `SeededAt` | İlk deploy anı; seeder dosyaları bu damgaya göre sıralar. Sunucu üretir, düzenlemede korunur. |
| `ListFormCode` | Ekranın kodu; route, yetki adları ve dil anahtarları bu koddan türer. Dosyada `WizardCode` adıyla, üçüncü sırada yazılır. |
| `MenuCode` | Menü kaydının kodu; yetki adlarının kökü de budur. |
| `MenuUrl` | List'te `/admin/list/{MenuCode}` hesaplanır; Custom'da bileşenin `RoutePath`'i yazılır. |
| `CustomComponentName` | Yalnızca Custom yolunda; bağlanacak bileşenin adı. |
@ -600,7 +644,11 @@ Elle dosya yazarken buraya var olan bir kaydı koyma — silme onu da götürür
| Alan | Anlam |
| --- | --- |
| `CreateMenu` | `false` ise menü ve üst menü kaydı **hiç üretilmez**; ListForm, yetki ve dil anahtarları yine üretilir. SubGrid/parça olarak kullanılacak ekranlar için. Varsayılan `true`. |
> Dosyada bu bloğun tamamı `Menu` başlığı altındadır; `MenuParentModuleId` ise kök seviyede
> `ModuleId` adıyla yazılır. Menüsüz wizard'da da blok korunur: `MenuCode` yetkilerin kökü,
> Custom yolunda `MenuUrl` rotanın kaynağıdır.
| `CreateMenu` | `false` ise menü ve üst menü kaydı **hiç üretilmez**; ListForm, yetki ve dil anahtarları yine üretilir. SubGrid/parça olarak kullanılacak ekranlar için. Varsayılan `true` olduğu için dosyaya **yalnızca `false` iken** yazılır. |
| `MenuParentCode` / `MenuParentModuleId` / `MenuParentIcon` | Üst menü; yoksa oluşturulur. |
| `MenuIcon` | React-icons adı (`FcBiohazard`, `FaBox`…). |
| `MenuOrder` | Kardeşler arası sıra. |
@ -609,6 +657,9 @@ Elle dosya yazarken buraya var olan bir kaydı koyma — silme onu da götürür
| Alan | Anlam |
| --- | --- |
> `LanguageText*` alanlarının tamamı dosyada `Language` başlığı altındadır. `ListForm` yalnızca
> dil **anahtarını** taşır (`Title = "{ListFormCode}.Title"`), metinlerin tek kaynağı burasıdır.
| `PermissionGroupName` | Var olan grup ya da yeni grup adı. |
| `PermissionGroupDisplayNameEn` / `Tr` | Grup adıyla aynı dil anahtarına yazılır. Var olan grup seçilip boş bırakılırsa sunucu veritabanındaki değeri geri yazar. |
| `LanguageTextMenuEn` / `Tr` | Menü etiketi. |
@ -624,23 +675,23 @@ edilir.
| Alan | Anlam |
| --- | --- |
| `DataSourceCode` | Bağlantı kodu; varsayılan `"Default"`. |
| `DataSourceConnectionString` | Yeni bir bağlantı tanımlanıyorsa; aksi hâlde boş. **Asla gerçek kimlik bilgisi yazma.** |
| `SelectCommandType` | Bkz. §3. |
| `SelectCommand` | Tablo/view adı, fonksiyon çağrısı ya da sorgu metni. |
| `KeyFieldName` / `KeyFieldDbSourceType` | Birincil anahtar ve tipi. |
| `DataSourceCode` | `[ListForm]` Bağlantı kodu; varsayılan `"Default"`. |
| `DataSourceConnectionString` | Yalnızca **yeni** bir bağlantı tanımlanıyorsa doldurulur; boşsa dosyaya hiç yazılmaz. **Asla gerçek kimlik bilgisi yazma.** |
| `SelectCommandType` | `[ListForm]` Bkz. §3. |
| `SelectCommand` | `[ListForm]` Tablo/view adı, fonksiyon çağrısı ya da sorgu metni. |
| `KeyFieldName` / `KeyFieldDbSourceType` | `[ListForm]` Birincil anahtar ve tipi. |
**Davranış**
| Alan | Anlam |
| --- | --- |
| `IsTenant` / `IsBranch` / `IsOrganizationUnit` | Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. |
| `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `ConfirmDelete` | Toolbar ve satır aksiyonları. |
| `AllowDetail` | Satırda **Detay** düğmesi çıkar; `/admin/form/{ListFormCode}/{anahtar}` adresini yeni sekmede açar — aynı ekranın tek kayıtlık form görünümüdür. `KeyFieldName` boşsa çalışmaz. |
| `DefaultLayout` | `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. |
| `Grid` `Card` `Pivot` `Chart` `Tree` `Gantt` `Scheduler` `Todo` | Hangi görünümlerin açık olduğu. Açtığın her görünümün option bloğunu da doldur. |
| `IsTenant` / `IsBranch` / `IsOrganizationUnit` | `[ListForm]` Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. |
| `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `ConfirmDelete` | `[ListForm]` Toolbar ve satır aksiyonları (`EditingOptionDto`). |
| `AllowDetail` | `[ListForm]` Satırda **Detay** düğmesi çıkar; `/admin/form/{ListFormCode}/{anahtar}` adresini yeni sekmede açar — aynı ekranın tek kayıtlık form görünümüdür. `KeyFieldName` boşsa çalışmaz. |
| `DefaultLayout` | `[ListForm]` `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. |
| `Grid` `Card` `Pivot` `Chart` `Tree` `Gantt` `Scheduler` `Todo` | `[ListForm]` Hangi görünümlerin açık olduğu (`LayoutDto`). Açtığın her görünümün option bloğunu da doldur. |
**Görünüm option blokları** — yalnızca ilgili bayrak `true` ise anlamlıdır:
**Görünüm option blokları** — `[ListForm]`; yalnızca ilgili bayrak `true` ise anlamlıdır:
| Blok | Zorunlu alanlar |
| --- | --- |
@ -666,6 +717,11 @@ kaynağı odur. Grup yapısı yalnızca düzenleme formunun yerleşimini belirle
]
```
Seed dosyasında `Groups` **kırpılmış** hâlde durur: grup başlığı/kolon sayısı ve alanların
editör bilgileri `ListForm.EditingFormDto` ve `Fields` içinde olduğu için tekrarlanmaz; dosyada
her öğe yalnızca `FieldName`, `CaptionName` ve `TurkishCaption`/`EnglishCaption` taşır — çünkü
alan başlıklarının dil metinlerinin başka kaynağı yoktur. Okurken tümü geri doldurulur.
Her `Items` öğesi:
| Alan | Anlam |
@ -673,16 +729,16 @@ Her `Items` öğesi:
| `FieldName` | Veri kolonunun adı (SQL'deki adıyla birebir). |
| `CaptionName` | Dil anahtarı ya da düz başlık. |
| `TurkishCaption` / `EnglishCaption` | Dil metinleri; ikisi de doldurulur. |
| `EditorType` | Bkz. §3. |
| `EditorOptions` | DevExtreme editör JSON'u — §6. |
| `EditorScript` | Alan davranış script'i — §5. |
| `DbSourceType` | Bkz. §3. |
| `IsRequired` | Zorunluluk. |
| `IncludeInEditingForm` | `false` ise yalnızca grid sütunu olur, forma girmez. |
| `ColSpan` | Formda kaç kolon kaplar (grubun `ColCount` değeri içinde). |
| `LookupDataSourceType` | `1/2/3` — bkz. §3. `0` = lookup yok. |
| `LookupQuery` | `Query` tipinde SQL; `StaticData` tipinde JSON dizisi; `WebService` tipinde URL. |
| `ValueExpr` / `DisplayExpr` | Lookup'ın değer ve etiket kolonları. |
| `EditorType` | `[Fields]` Bkz. §3. |
| `EditorOptions` | `[Fields]` DevExtreme editör JSON'u — §6. |
| `EditorScript` | `[Fields]` Alan davranış script'i — §5. |
| `DbSourceType` | `[Fields]` Bkz. §3. |
| `IsRequired` | `[Fields]` Zorunluluk (`ValidationRuleDto`). |
| `IncludeInEditingForm` | `[Fields]` `false` ise yalnızca grid sütunu olur, forma girmez (`EditGroupOrderNo`). |
| `ColSpan` | `[Fields]` Formda kaç kolon kaplar (grubun `ColCount` değeri içinde). |
| `LookupDataSourceType` | `[Fields]` `1/2/3` — bkz. §3. `0` = lookup yok. |
| `LookupQuery` | `[Fields]` `Query` tipinde SQL; `StaticData` tipinde JSON dizisi; `WebService` tipinde URL. |
| `ValueExpr` / `DisplayExpr` | `[Fields]` Lookup'ın değer ve etiket kolonları. |
### 4.3.1 Sütunlar nasıl otomatik üretilir
@ -990,6 +1046,7 @@ varsayılan değeri olur ve **yeni kayıtta o alanı otomatik doldurur**.
"Wizard": {
"ComponentKind": 0,
"WizardName": "Orders",
"SeededAt": "2026-08-26T10:15:00",
"ListFormCode": "App.Mrp.Orders",
"MenuCode": "App.Mrp.Orders",
"MenuOrder": 1,
@ -1212,7 +1269,7 @@ widget, sorgunun döndürdüğü **her satır için bir kart** çizer:
Ayrıntı: `lowcode-reference.instructions.md` §5.3.
Alt form olarak kullanılacak ekranı **menüsüz wizard** (`CreateMenu: false`) ile üret; menüde
görünmesin ama kendi yetkileri ve alan tanımları olsun. Dosya adının zaman damgası ana
görünmesin ama kendi yetkileri ve alan tanımları olsun. `Wizard.SeededAt` damgası ana
wizard'ınkinden **küçük** olmalıdır (§0.2).
### 4.7 İş akışı (onay süreci)
@ -2311,7 +2368,7 @@ Bu çift, custom component'in menüye nasıl bağlandığının çalışan örne
```
configs/seeds/host/custom/RoleComponent.json
configs/seeds/host/crud/AbpRoles.json · AbpUsers.json · AbpUserRoles.json
configs/seeds/host/wizard/20260819131500_RoleList.json
configs/seeds/host/wizard/RoleList.json
```
Nasıl bağlanıyorlar:
@ -3419,8 +3476,8 @@ configs/seeds/host/
├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…)
├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir
├── custom/AdvanceRequestForm.json ← yalnızca serbest yerleşim isteniyorsa
├── wizard/20260826101000_AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, önce
└── wizard/20260826101500_AdvanceRequests.json ← ana ekran + menü + yetki + dil + onay akışı
├── wizard/AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, küçük SeededAt
└── wizard/AdvanceRequests.json ← ana ekran + menü + yetki + dil + onay akışı
```
**Karar:** Talep formu **standart bir kayıt listesi + düzenleme formu + onay akışı** ise
@ -3483,7 +3540,7 @@ tedarikçi lookup'ı + iki kademeli onay).
Prompt ile üretim yaptıysan ek olarak:
- [ ] Dosyalar doğru kapsam klasöründe (`host/` ya da `tenants/{tenantId}/`).
- [ ] Üretim sırası doğru (§0.2) ve bağımlı wizard'ın zaman damgası daha büyük.
- [ ] Üretim sırası doğru (§0.2) ve bağımlı wizard'ın `SeededAt` damgası daha büyük.
- [ ] Tablo/kolon adları uydurulmadı; ya var olan nesneden okundu ya da `sql/object/` altında
üretildi.
- [ ] `KeyFieldDbSourceType`, tablodaki `Id` tipiyle uyumlu.

View file

@ -487,7 +487,7 @@ butonlar gösterilebilir.
5. **Permission** kayıtları (`.Default`, `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note`)
6. **Dil anahtarları** (menü, başlık, açıklama; EN + TR)
7. Gerekiyorsa **ayar/entegrasyon** bağımlılıkları
8. **Seed dosyası**`configs/seeds/{host|tenants/{tenantId}}/wizard/{zaman damgası}_{Ad}.json`
8. **Seed dosyası**`configs/seeds/{host|tenants/{tenantId}}/wizard/{Ad}.json` (wizard cevapları + üretilen `ListForm`/`Fields` sözleşmesi)
9. **Doğrulama ve geri alma** notları
**Adım bileşenleri.** Adımlar numaralı dosya adları yerine işlevleriyle adlandırılır ve
@ -509,6 +509,16 @@ adım sayısından bağımsızdır.
Menü ağacı yardımcıları (`menuTree.ts`) adım bileşenlerinden ayrı bir modüldedir; böylece Wizard
ve `SqlTableDesignerDialog` bu fonksiyonları kullanırken adım bileşenlerini pakete çekmez.
**Seed dosyası ekranın kendisini taşır.** Dosya `Wizard` bölümünün yanında ekranın
`GridOptionsEditDto` sözleşmesini (`ListForm`) ve alanların `ColumnFormatEditDto` sözleşmesini
(`Fields`) tutar; `WizardDataSeeder` ekranı bu iki bölümden kurar. Böylece Wizard ile ListForm
detayı aynı veri yapısını konuşur ve tasarımcıdaki değişiklikler wizard'a geri yazılabilir.
`Wizard` bölümü yalnızca ekrandan üretilemeyen bilgileri (menü, yetki, dil metinleri, bileşen
yolu, alan başlıklarının dil karşılıkları) taşır; `ListForm`/`Fields` içinde karşılığı olan alanlar
dosyaya yazılmaz, okuma sırasında geri doldurulur. Kalanlar dosyada `Menu`, `Language` ve `Groups`
başlıkları altında toplanır (`WizardSeedFileDto.ToSeedJson` / `FromSeedJson`). Dosya adı `{Ad}.json`'dır (zaman damgası yok);
uygulama sırası `Wizard.SeededAt` damgasından gelir.
**İki yol: `ComponentKind`.** `ListFormWizardDto.ComponentKind` sihirbazın yol ayrımıdır ve seed
dosyasında ilk alandır. Varsayılan `List` olduğu için bu alanı taşımayan eski seed dosyaları
eskisi gibi çalışır.
@ -1165,7 +1175,7 @@ geri yüklenir. Kapsam klasörü CDN düzeniyle aynıdır ve `SeedPathResolver`
```
configs/seeds/
├── host/ # host kapsamı
│ ├── wizard/ # {zaman damgası}_{Ad}.json → WizardDataSeeder
│ ├── wizard/ # {Ad}.json → WizardDataSeeder
│ ├── custom/ # {ComponentName}.json → CustomComponentDataSeeder
│ ├── crud/ # {EntityName}.json → CrudDataSeeder
│ ├── sql/{object,execute}/ # .sql (SQL Server)
@ -1176,7 +1186,9 @@ configs/seeds/
> Bu dosyalar elle de düzenlenebilir; ilgili ekrandan tekrar kaydedildiğinde yeniden üretilirler.
> Wizard dosyası `EditFileName` ile güncellenirse sunucu önce eski dosyayı ve ürettiği kayıtları
> siler, sonra yenisini üretir.
> siler, sonra yenisini üretir. Wizard'ın ürettiği ekran **ListForm detayından** düzenlendiğinde
> de aynı dosya güncellenir (`WizardSeedSynchronizer`): `ListForm`/`Fields` bölümleri ve
> `Wizard` bölümündeki karşılıkları veritabanındaki güncel haliyle değiştirilir.
---

View file

@ -23,5 +23,51 @@ public class ColumnFormatEditDto : ColumnFormatDto
}
set { JoinTableJson = JsonSerializer.Serialize(value); }
}
/// <summary>ListFormField sozlesmesini kayit nesnesine yazar.</summary>
public void ApplyTo(IListFormFieldArtifact target)
{
target.ListFormCode = ListFormCode;
target.UserId = UserId;
target.RoleId = RoleId;
target.CultureName = CultureName;
target.FieldName = FieldName;
target.CaptionName = CaptionName;
target.PlaceHolder = PlaceHolder;
target.BandName = BandName;
target.Visible = Visible;
target.IsActive = IsActive;
target.Width = Width;
target.ListOrderNo = ListOrderNo;
target.SourceDbType = SourceDbType;
target.SortIndex = SortIndex;
target.SortDirection = SortDirection;
target.AllowSearch = AllowSearch;
target.AllowEditing = AllowEditing;
target.AllowAdding = AllowAdding;
target.Alignment = Alignment;
target.Format = Format;
target.EditOrderNo = EditOrderNo;
target.EditGroupOrderNo = EditGroupOrderNo;
target.EditorType2 = EditorType2;
target.ColSpan = ColSpan;
target.EditorOptions = EditorOptions;
target.EditorScript = EditorScript;
target.ColumnCssClass = ColumnCssClass;
target.ColumnCssValue = ColumnCssValue;
target.ColumnFilterJson = ColumnFilterJson;
target.ColumnHeaderJson = ColumnHeaderJson;
target.GroupingJson = GroupingJson;
target.ColumnCustomizationJson = ColumnCustomizationJson;
target.TotalSummaryJson = TotalSummaryJson;
target.GroupSummaryJson = GroupSummaryJson;
target.JoinTableJson = JoinTableJson;
target.LookupJson = LookupJson;
target.ValidationRuleJson = ValidationRuleJson;
target.ColumnStylingJson = ColumnStylingJson;
target.PermissionJson = PermissionJson;
target.PivotSettingsJson = PivotSettingsJson;
}
}

View file

@ -123,5 +123,108 @@ public class GridOptionsEditDto : GridOptionsDto
}
set { ExtraFilterJson = JsonSerializer.Serialize(value); }
}
/// <summary>
/// ListForm sozlesmesini kayit nesnesine yazar. Seed dosyasindan okunan ekran ile tasarimcinin
/// urettigi ekranin ayni kolonlari doldurmasi bu tek noktadan garanti edilir.
/// </summary>
public void ApplyTo(IListFormArtifact target)
{
target.ListFormCode = ListFormCode;
target.CultureName = CultureName;
target.Name = Name;
target.Title = Title;
target.Description = Description;
target.ListFormType = ListFormType;
target.IsSubForm = IsSubForm;
target.SubFormsListFormType = SubFormsListFormType;
target.ShowNote = ShowNote;
target.SortMode = SortMode;
target.PageSize = PageSize;
target.Width = Width;
target.Height = Height;
target.FullHeight = FullHeight;
target.UserId = UserId;
target.RoleId = RoleId;
target.DataSourceCode = DataSourceCode;
target.SelectCommandType = SelectCommandType;
target.SelectCommand = SelectCommand;
target.TableName = TableName;
target.KeyFieldName = KeyFieldName;
target.KeyFieldDbSourceType = KeyFieldDbSourceType;
target.IsTenant = IsTenant;
target.IsBranch = IsBranch;
target.IsOrganizationUnit = IsOrganizationUnit;
target.DefaultFilter = DefaultFilter;
target.InsertServiceAddress = InsertServiceAddress;
target.InsertCommand = InsertCommand;
target.InsertBeforeCommand = InsertBeforeCommand;
target.InsertAfterCommand = InsertAfterCommand;
target.UpdateServiceAddress = UpdateServiceAddress;
target.UpdateCommand = UpdateCommand;
target.UpdateBeforeCommand = UpdateBeforeCommand;
target.UpdateAfterCommand = UpdateAfterCommand;
target.DeleteServiceAddress = DeleteServiceAddress;
target.DeleteCommand = DeleteCommand;
target.DeleteBeforeCommand = DeleteBeforeCommand;
target.DeleteAfterCommand = DeleteAfterCommand;
target.SelectFieldsDefaultValueJson = SelectFieldsDefaultValueJson;
target.InsertFieldsDefaultValueJson = InsertFieldsDefaultValueJson;
target.UpdateFieldsDefaultValueJson = UpdateFieldsDefaultValueJson;
target.DeleteFieldsDefaultValueJson = DeleteFieldsDefaultValueJson;
target.FormFieldsDefaultValueJson = FormFieldsDefaultValueJson;
target.LayoutJson = LayoutJson;
target.FilterRowJson = FilterRowJson;
target.RowJson = RowJson;
target.HeaderFilterJson = HeaderFilterJson;
target.FilterPanelJson = FilterPanelJson;
target.SearchPanelJson = SearchPanelJson;
target.GroupPanelJson = GroupPanelJson;
target.SelectionJson = SelectionJson;
target.ColumnOptionJson = ColumnOptionJson;
target.PivotOptionJson = PivotOptionJson;
target.TreeOptionJson = TreeOptionJson;
target.GanttOptionJson = GanttOptionJson;
target.SchedulerOptionJson = SchedulerOptionJson;
target.TodoOptionJson = TodoOptionJson;
target.PagerOptionJson = PagerOptionJson;
target.EditingOptionJson = EditingOptionJson;
target.EditingFormJson = EditingFormJson;
target.PermissionJson = PermissionJson;
target.CommandColumnJson = CommandColumnJson;
target.StateStoringJson = StateStoringJson;
target.CustomJsSourcesJson = CustomJsSourcesJson;
target.CustomStyleSourcesJson = CustomStyleSourcesJson;
target.SubFormsJson = SubFormsJson;
target.WidgetsJson = WidgetsJson;
target.WorkflowJson = WorkflowJson;
target.ExtraFilterJson = ExtraFilterJson;
target.CommonJson = CommonJson;
target.AdaptiveLayoutJson = AdaptiveLayoutJson;
target.AnimationJson = AnimationJson;
target.AnnotationsJson = AnnotationsJson;
target.ArgumentAxisJson = ArgumentAxisJson;
target.CommonAnnotationsSettingsJson = CommonAnnotationsSettingsJson;
target.CommonAxisSettingsJson = CommonAxisSettingsJson;
target.CommonPaneSettingsJson = CommonPaneSettingsJson;
target.CommonSeriesSettingsJson = CommonSeriesSettingsJson;
target.CrosshairJson = CrosshairJson;
target.ExportJson = ExportJson;
target.LegendJson = LegendJson;
target.MarginJson = MarginJson;
target.PanesJson = PanesJson;
target.ScrollBarJson = ScrollBarJson;
target.SeriesJson = SeriesJson;
target.SizeJson = SizeJson;
target.TitleJson = TitleJson;
target.TooltipJson = TooltipJson;
target.ValueAxisJson = ValueAxisJson;
target.ZoomAndPanJson = ZoomAndPanJson;
}
}

View file

@ -1,10 +1,25 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Sozsoft.Languages.Languages;
using Sozsoft.Platform.Enums;
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Wizard adimlarinin cevaplari ve bu cevaplardan ekran sozlesmesini ureten donusumler.
/// <para>
/// Deploy (<c>ListFormWizardAppService</c>), seed (<c>WizardDataSeeder</c>) ve tasarimci
/// senkronizasyonu (<c>WizardSeedSynchronizer</c>) ayni donusumleri buradan kullanir; boylece
/// uc yolun ciktisi birbirinden ayrisamaz. Entity'ye yazma islemi
/// <see cref="GridOptionsEditDto.ApplyTo"/> ve <see cref="ColumnFormatEditDto.ApplyTo"/>
/// uzerinden, Domain.Shared'deki kayit sozlesmeleriyle yapilir; boylece entity katmanini
/// tanimayan DbMigrator da ayni donusumu kullanabilir.
/// </para>
/// </summary>
public class ListFormWizardDto
{
/// <summary>
@ -23,6 +38,13 @@ public class ListFormWizardDto
public string EditFileName { get; set; }
public string WizardName { get; set; }
/// <summary>
/// Wizard'in ilk deploy edildigi an. Dosya adi tarih tasimadigi icin seeder dosyalari bu
/// damgaya gore siralar; birbirine bagli wizard'lar (once Customers, sonra Tasks) uretildikleri
/// sirayla uygulanir. Duzenlemede ilk deger korunur.
/// </summary>
public DateTime SeededAt { get; set; }
public string ListFormCode { get; set; }
public string MenuCode { get; set; }
public int MenuOrder { get; set; }
@ -107,4 +129,450 @@ public class ListFormWizardDto
public List<WizardColumnGroupInputDto> Groups { get; set; } = new();
public List<SubFormDto> SubForms { get; set; } = new();
public List<WidgetEditDto> Widgets { get; set; } = new();
/// <summary>Wizard'in menu/permission kodu; ad ve kod bos gelebildigi icin tek kuraldan cozulur.</summary>
public string ResolveMenuCode()
{
return string.IsNullOrWhiteSpace(MenuCode)
? WizardConsts.WizardKey(WizardName?.Trim())
: MenuCode.Trim();
}
/// <summary>Wizard'in ListForm kodu; bos gelirse menu kodu ile aynidir.</summary>
public string ResolveListFormCode()
{
return string.IsNullOrWhiteSpace(ListFormCode)
? ResolveMenuCode()
: ListFormCode.Trim();
}
/// <summary>
/// Wizard girdisinden ListForm sozlesmesini uretir. <paramref name="isDeletedField"/> ve
/// <paramref name="isCreatedField"/> hedef tablonun soft delete / audit kolonlarini tasiyip
/// tasimadigini bildirir; silme komutu ve varsayilan alan degerleri buna gore kurulur.
/// </summary>
public GridOptionsEditDto BuildListForm(bool isDeletedField, bool isCreatedField)
{
var code = ResolveMenuCode();
var listFormCode = ResolveListFormCode();
var titleLangKey = $"{listFormCode}.Title";
var descLangKey = $"{listFormCode}.Desc";
var workflow = NormalizeWorkflow(WorkflowDto, listFormCode);
WorkflowDto = workflow;
var groups = Groups ?? [];
var subForms = SubForms ?? [];
var widgets = Widgets ?? [];
var editingFormDtos = groups
.Select((g, gi) => new { Group = g, Order = gi + 1 })
.Where(x => x.Group.Items.Any(i => i.IncludeInEditingForm && i.FieldName != KeyFieldName))
.Select(x => new EditingFormDto
{
Order = x.Order,
Caption = x.Group.Caption,
ColCount = x.Group.ColCount,
ColSpan = x.Group.ColCount,
ItemType = "group"
})
.ToList();
var (formWidth, formHeight) = CalculateEditFormSize();
var dto = new GridOptionsEditDto
{
ListFormCode = listFormCode,
CultureName = LanguageCodes.En,
ListFormType = ListFormTypeEnum.List,
Name = code,
Title = titleLangKey,
Description = descLangKey,
PageSize = 10,
IsSubForm = false,
ShowNote = subForms.Count > 0 || workflow.Criteria.Count > 0,
SortMode = PlatformConsts.GridOptions.SortModeSingle,
DataSourceCode = DataSourceCode,
IsTenant = IsTenant,
IsBranch = IsBranch,
IsOrganizationUnit = IsOrganizationUnit,
SelectCommandType = SelectCommandType,
SelectCommand = SelectCommand,
KeyFieldName = KeyFieldName,
KeyFieldDbSourceType = KeyFieldDbSourceType,
DefaultFilter = isDeletedField ? WizardConsts.DefaultFilterJson : null,
DeleteCommand = isDeletedField ? WizardConsts.DefaultDeleteCommand(SelectCommand) : null,
};
// Varsayilanlar WizardConsts uzerinden JSON olarak kurulur; DTO'nun nesne karsiliklari
// (FilterRowDto, EditingOptionDto ...) bu JSON'lardan turedigi icin seed dosyasi ayni
// degerleri nesne olarak tasir.
dto.ExportJson = WizardConsts.DefaultExportJson;
dto.LayoutJson = WizardConsts.DefaultLayoutJson(
DefaultLayout: DefaultLayout,
Grid: Grid,
Card: Card,
Pivot: Pivot,
Chart: Chart,
Tree: Tree,
Gantt: Gantt,
Scheduler: Scheduler,
Todo: Todo);
dto.FilterRowJson = WizardConsts.DefaultFilterRowJson();
dto.HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson();
dto.SearchPanelJson = WizardConsts.DefaultSearchPanelJson();
dto.GroupPanelJson = WizardConsts.DefaultGroupPanelJson();
dto.SelectionJson = WizardConsts.DefaultSelectionSingleJson(workflow.Criteria.Count > 0
? PlatformConsts.GridOptions.SelectionModeSingle
: PlatformConsts.GridOptions.SelectionModeNone);
dto.ColumnOptionJson = WizardConsts.DefaultColumnOptionJson();
dto.PermissionJson = WizardConsts.DefaultPermissionJson(code);
dto.PagerOptionJson = WizardConsts.DefaultPagerOptionJson;
dto.EditingOptionJson = WizardConsts.DefaultEditingOptionJson(
titleLangKey, formWidth, formHeight,
AllowDeleting, AllowAdding, AllowUpdating,
ConfirmDelete, false, AllowDetail);
dto.DeleteFieldsDefaultValueJson = isDeletedField
? WizardConsts.DefaultDeleteFieldsDefaultValueJson(KeyFieldDbSourceType)
: WizardConsts.DefaultDeleteFieldsJsonOnlyId(KeyFieldDbSourceType);
dto.InsertFieldsDefaultValueJson = isCreatedField
? WizardConsts.DefaultInsertFieldsDefaultValueJson(KeyFieldDbSourceType)
: WizardConsts.DefaultInsertFieldsJsonOnlyId(KeyFieldDbSourceType);
dto.EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null;
dto.SubFormsJson = subForms.Count > 0 ? JsonSerializer.Serialize(subForms) : null;
dto.WidgetsJson = widgets.Count > 0 ? JsonSerializer.Serialize(widgets) : null;
dto.WorkflowJson = HasWorkflow(workflow) ? JsonSerializer.Serialize(workflow) : null;
dto.TreeOptionJson = UsesLayout("tree", Tree) &&
!string.IsNullOrEmpty(TreeOptionDto?.ParentIdExpr)
? JsonSerializer.Serialize(TreeOptionDto)
: null;
dto.GanttOptionJson = UsesLayout("gantt", Gantt) &&
!string.IsNullOrEmpty(GanttOptionDto?.ParentIdExpr)
? JsonSerializer.Serialize(GanttOptionDto)
: null;
dto.SchedulerOptionJson = UsesLayout("scheduler", Scheduler) &&
!string.IsNullOrEmpty(SchedulerOptionDto?.TextExpr)
? JsonSerializer.Serialize(SchedulerOptionDto)
: null;
dto.TodoOptionJson = UsesLayout("todo", Todo) &&
!string.IsNullOrEmpty(TodoOptionDto?.TitleExpr) &&
!string.IsNullOrEmpty(TodoOptionDto?.StatusExpr)
? JsonSerializer.Serialize(TodoOptionDto)
: null;
return dto;
}
/// <summary>
/// Wizard gruplarindaki her alani ListFormField sozlesmesine cevirir. Alan sirasi gruplarin
/// sirasini izler; anahtar alan listede gizlenir ve duzenleme formuna alinmaz.
/// </summary>
public List<ColumnFormatEditDto> BuildFields()
{
var code = ResolveMenuCode();
var listFormCode = ResolveListFormCode();
var fields = new List<ColumnFormatEditDto>();
var fieldOrder = 0;
var editGroupOrder = 0;
foreach (var group in Groups ?? [])
{
editGroupOrder++;
var editOrder = 0;
foreach (var item in group.Items ?? [])
{
fieldOrder++;
var inEditingForm = item.IncludeInEditingForm && item.FieldName != KeyFieldName;
if (inEditingForm)
editOrder++;
var field = new ColumnFormatEditDto
{
ListFormCode = listFormCode,
FieldName = item.FieldName,
CaptionName = item.CaptionName,
Visible = item.FieldName != KeyFieldName,
IsActive = true,
AllowSearch = true,
Width = 0, // Fit columns icin tum alanlar sifir genislikle baslar.
ListOrderNo = fieldOrder,
EditGroupOrderNo = item.IncludeInEditingForm ? editGroupOrder : null,
EditOrderNo = inEditingForm ? editOrder : null,
EditorType2 = item.EditorType,
ColSpan = item.ColSpan,
EditorOptions = string.IsNullOrWhiteSpace(item.EditorOptions) ? null : item.EditorOptions,
EditorScript = string.IsNullOrWhiteSpace(item.EditorScript) ? null : item.EditorScript,
SourceDbType = item.DbSourceType,
CultureName = PlatformConsts.DefaultLanguage,
};
field.PermissionJson = WizardConsts.DefaultFieldPermissionJson(code);
field.ColumnCustomizationJson = WizardConsts.DefaultColumnCustomizationJson;
field.ColumnFilterJson = WizardConsts.DefaultColumnFilteringJson;
field.PivotSettingsJson = WizardConsts.DefaultPivotSettingsJson;
field.LookupJson = !string.IsNullOrWhiteSpace(item.LookupQuery)
? WizardConsts.DefaultLookupJson(item.LookupDataSourceType, item.DisplayExpr, item.ValueExpr, item.LookupQuery)
: null;
field.ValidationRuleJson = inEditingForm && item.IsRequired
? WizardConsts.DefaultValidationRuleRequiredJson
: null;
fields.Add(field);
}
}
return fields;
}
/// <summary>
/// Tasarimci uzerinde degisen ekrani wizard bolumune geri yazar. Wizard adimlarinin sordugu
/// her sorunun ListForm sozlesmesinde bir karsiligi vardir; senkron kalmazsa wizard yeniden
/// deploy edildiginde tasarimcida yapilan degisiklikler geri alinirdi.
/// </summary>
public void RefreshFrom(GridOptionsEditDto listForm, IReadOnlyList<ColumnFormatEditDto> fields)
{
ListFormCode = listForm.ListFormCode;
DataSourceCode = listForm.DataSourceCode;
SelectCommandType = listForm.SelectCommandType;
SelectCommand = listForm.SelectCommand;
KeyFieldName = listForm.KeyFieldName;
KeyFieldDbSourceType = listForm.KeyFieldDbSourceType;
IsTenant = listForm.IsTenant;
IsBranch = listForm.IsBranch;
IsOrganizationUnit = listForm.IsOrganizationUnit;
var layout = listForm.LayoutDto;
DefaultLayout = layout.DefaultLayout;
Grid = layout.Grid;
Card = layout.Card;
Pivot = layout.Pivot;
Tree = layout.Tree;
Chart = layout.Chart;
Gantt = layout.Gantt;
Scheduler = layout.Scheduler;
Todo = layout.Todo;
var editing = listForm.EditingOptionDto;
AllowAdding = editing.AllowAdding;
AllowUpdating = editing.AllowUpdating;
AllowDeleting = editing.AllowDeleting;
AllowDetail = editing.AllowDetail;
ConfirmDelete = editing.ConfirmDelete;
TreeOptionDto = listForm.TreeOptionDto;
GanttOptionDto = listForm.GanttOptionDto;
SchedulerOptionDto = listForm.SchedulerOptionDto;
TodoOptionDto = listForm.TodoOptionDto;
WorkflowDto = listForm.WorkflowDto;
SubForms = [.. listForm.SubFormsDto];
Widgets = [.. listForm.WidgetsDto];
Groups = BuildGroups(listForm, fields);
}
/// <summary>
/// Alanlari wizard'in grup/oge modeline geri cevirir. Grup basliklari ve kolon sayilari
/// duzenleme formu tanimindan, alan sirasi ise listedeki sirasindan gelir. Alan basliklarinin
/// iki dilli metinleri ListForm alan kaydinda tutulmadigi icin mevcut wizard cevaplarindan
/// tasinir; aksi halde ekran tasarimcidan kaydedildiginde dil metinleri kaybolurdu.
/// </summary>
private List<WizardColumnGroupInputDto> BuildGroups(GridOptionsEditDto listForm, IReadOnlyList<ColumnFormatEditDto> fields)
{
var editingForm = listForm.EditingFormDto ?? [];
var captions = (Groups ?? [])
.SelectMany(g => g.Items ?? [])
.Where(i => !string.IsNullOrWhiteSpace(i.FieldName))
.GroupBy(i => i.FieldName, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
var groups = new List<WizardColumnGroupInputDto>();
var groupIndexByOrder = new Dictionary<int, int>();
foreach (var field in fields.OrderBy(f => f.ListOrderNo))
{
// Duzenleme formunda yer almayan alanlar da wizard'da gorunmelidir; grup bilgisi
// olmadigi icin ilk gruba, duzenleme disi olarak eklenirler.
var groupOrder = field.EditGroupOrderNo ?? 1;
if (!groupIndexByOrder.TryGetValue(groupOrder, out var index))
{
var definition = editingForm.FirstOrDefault(x => x.Order == groupOrder);
groups.Add(new WizardColumnGroupInputDto
{
Caption = definition?.Caption ?? string.Empty,
ColCount = definition?.ColCount ?? 2,
});
index = groups.Count - 1;
groupIndexByOrder[groupOrder] = index;
}
var lookup = field.LookupDto;
captions.TryGetValue(field.FieldName ?? string.Empty, out var previous);
groups[index].Items.Add(new WizardColumnItemInputDto
{
FieldName = field.FieldName,
CaptionName = field.CaptionName,
EditorType = field.EditorType2,
EditorOptions = field.EditorOptions,
EditorScript = field.EditorScript,
ColSpan = field.ColSpan ?? 1,
IsRequired = field.ValidationRuleDto.Exists(r =>
string.Equals(r.Type, nameof(UiColumnValidationRuleTypeEnum.required), StringComparison.OrdinalIgnoreCase)),
IncludeInEditingForm = field.EditGroupOrderNo.HasValue,
DbSourceType = field.SourceDbType,
LookupDataSourceType = lookup.DataSourceType,
ValueExpr = lookup.ValueExpr,
DisplayExpr = lookup.DisplayExpr,
LookupQuery = lookup.LookupQuery,
EnglishCaption = previous?.EnglishCaption,
TurkishCaption = previous?.TurkishCaption,
});
}
return groups;
}
/// <summary>
/// Duzenleme formunun genisligi ve yuksekligi gruplarin kolon sayisindan ve toplam satir
/// sayisindan hesaplanir; boylece popup icerigi kirpilmadan acilir.
/// </summary>
private (int Width, int Height) CalculateEditFormSize()
{
var groupLayouts = (Groups ?? [])
.Select(g =>
{
var itemCount = (g.Items ?? []).Count(i => i.IncludeInEditingForm && i.FieldName != KeyFieldName);
var columnCount = Math.Clamp(g.ColCount, 1, WizardConsts.EditFormMaxColumnCount);
return new
{
ColumnCount = columnCount,
RowCount = (int)Math.Ceiling(itemCount / (double)columnCount)
};
})
.ToList();
var formColumnCount = groupLayouts.Count > 0 ? groupLayouts.Max(x => x.ColumnCount) : 1;
var totalRowCount = groupLayouts.Sum(x => x.RowCount);
return (
WizardConsts.EditFormMaxWidth / WizardConsts.EditFormMaxColumnCount * formColumnCount,
WizardConsts.EditFormHeightDefault + totalRowCount * WizardConsts.EditFormHeightByRow);
}
private bool UsesLayout(string layout, bool enabled)
{
return enabled || string.Equals(DefaultLayout, layout, StringComparison.OrdinalIgnoreCase);
}
private static bool HasWorkflow(WorkflowDto workflow)
{
return workflow != null && (
!string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) ||
workflow.Criteria.Count > 0);
}
/// <summary>
/// Workflow adimlarini kaydedilebilir hale getirir: basliklar tekillestirilir, adim
/// kimlikleri ListForm koduyla on eklenir. Seed dosyasina normalize edilmis hali yazilir.
/// </summary>
private static WorkflowDto NormalizeWorkflow(WorkflowDto workflow, string listFormCode)
{
workflow ??= new WorkflowDto();
workflow.Criteria ??= [];
EnsureUniqueWorkflowCriteriaTitles(workflow.Criteria);
NormalizeWorkflowCriteriaIds(listFormCode, workflow.Criteria);
return workflow;
}
private static void EnsureUniqueWorkflowCriteriaTitles(List<ListFormWorkflowCriteriaDto> criteria)
{
if (criteria.Count == 0)
return;
var baseTitles = criteria
.Select(x => string.IsNullOrWhiteSpace(x.Title) ? NormalizeWorkflowTitleFallback(x.Kind) : x.Title.Trim())
.ToList();
var duplicateTitles = baseTitles
.GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
.Where(x => x.Count() > 1)
.Select(x => x.Key)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var titleCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var usedTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in criteria)
{
var baseTitle = string.IsNullOrWhiteSpace(item.Title) ? NormalizeWorkflowTitleFallback(item.Kind) : item.Title.Trim();
var title = baseTitle;
if (duplicateTitles.Contains(baseTitle))
{
titleCounts.TryGetValue(baseTitle, out var count);
count++;
titleCounts[baseTitle] = count;
title = $"{baseTitle}{count}";
}
if (usedTitles.Contains(title))
{
var index = 1;
var candidate = $"{title}{index}";
while (usedTitles.Contains(candidate))
{
index++;
candidate = $"{title}{index}";
}
title = candidate;
}
item.Title = title;
usedTitles.Add(title);
}
}
private static string NormalizeWorkflowTitleFallback(string kind)
{
return string.IsNullOrWhiteSpace(kind) ? "Step" : kind.Trim();
}
private static void NormalizeWorkflowCriteriaIds(string listFormCode, List<ListFormWorkflowCriteriaDto> criteria)
{
if (string.IsNullOrWhiteSpace(listFormCode) || criteria.Count == 0)
return;
foreach (var item in criteria)
{
item.Id = NormalizeWorkflowCriteriaId(listFormCode, item.Id);
item.ListFormCode = listFormCode;
item.NextOnStart = NormalizeWorkflowTargetId(listFormCode, item.NextOnStart);
item.NextOnTrue = NormalizeWorkflowTargetId(listFormCode, item.NextOnTrue);
item.NextOnFalse = NormalizeWorkflowTargetId(listFormCode, item.NextOnFalse);
item.NextOnApprove = NormalizeWorkflowTargetId(listFormCode, item.NextOnApprove);
item.NextOnReject = NormalizeWorkflowTargetId(listFormCode, item.NextOnReject);
foreach (var outcome in item.CompareOutcomes ?? [])
{
outcome.TargetId = NormalizeWorkflowTargetId(listFormCode, outcome.TargetId);
}
}
}
private static string NormalizeWorkflowTargetId(string listFormCode, string id)
{
return string.IsNullOrWhiteSpace(id) ? string.Empty : NormalizeWorkflowCriteriaId(listFormCode, id);
}
private static string NormalizeWorkflowCriteriaId(string listFormCode, string id)
{
var trimmed = id?.Trim();
if (string.IsNullOrWhiteSpace(trimmed))
return string.Empty;
if (trimmed.StartsWith($"{listFormCode}-", StringComparison.OrdinalIgnoreCase))
return trimmed;
return trimmed.StartsWith("N", StringComparison.OrdinalIgnoreCase)
? $"{listFormCode}-{trimmed}"
: trimmed;
}
}

View file

@ -1,5 +1,10 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Sozsoft.Platform.Enums;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.ListForms;
@ -12,6 +17,15 @@ public class WizardSeedFileDto
{
public ListFormWizardDto Wizard { get; set; }
/// <summary>
/// Wizard'in urettigi ekranin ListForm sozlesmesi. Tasarimci (ListForm detayi) uzerinde
/// yapilan her degisiklik buraya geri yazilir; seeder ekrani bu bolumden kurar.
/// </summary>
public GridOptionsEditDto ListForm { get; set; }
/// <summary>Ekranin alanlari; ListForm detayindaki alan tanimlariyla ayni sozlesme.</summary>
public List<ColumnFormatEditDto> Fields { get; set; } = [];
/// <summary>Tabloda IsDeleted alanı olup olmadığını belirtir (soft delete desteği).</summary>
public bool IsDeletedField { get; set; }
@ -23,6 +37,274 @@ public class WizardSeedFileDto
/// Daha önce var olan kayıtlar buraya eklenmez, silme işleminde sadece bu liste kullanılır.
/// </summary>
public WizardInsertedRecordsDto InsertedRecords { get; set; } = new();
/// <summary>
/// Dosyaya yazmadan once ortama ozgu alanlari temizler. Kayit kimlikleri ve audit damgalari
/// veritabanina aittir; dosyada tasinirlarsa her ortamda anlamsiz fark uretirler. EditType ise
/// yalnizca tasarimcinin hangi sekmeyi kaydettigini bildiren istek alanidir.
/// </summary>
private void ClearRuntimeFields()
{
if (Wizard != null)
Wizard.EditFileName = null;
if (ListForm != null)
ListForm.EditType = null;
ClearAudit(ListForm);
foreach (var field in Fields ?? [])
{
field.EditType = null;
ClearAudit(field);
}
}
/// <summary>
/// Ekranin sozlesmesi <see cref="ListForm"/> ve <see cref="Fields"/> bolumlerindedir; wizard
/// bolumunde ayni degerlerin ikinci bir kopyasi tutulmaz. Bu anahtarlar dosyaya yazilmadan
/// once wizard bolumunden cikarilir, okurken <see cref="Rehydrate"/> ile geri doldurulur.
/// </summary>
private static readonly string[] ListFormOwnedWizardKeys =
[
nameof(ListFormWizardDto.IsTenant), nameof(ListFormWizardDto.IsBranch), nameof(ListFormWizardDto.IsOrganizationUnit),
nameof(ListFormWizardDto.AllowAdding), nameof(ListFormWizardDto.AllowUpdating), nameof(ListFormWizardDto.AllowDeleting),
nameof(ListFormWizardDto.AllowDetail), nameof(ListFormWizardDto.ConfirmDelete),
nameof(ListFormWizardDto.DefaultLayout), nameof(ListFormWizardDto.Grid), nameof(ListFormWizardDto.Card),
nameof(ListFormWizardDto.Pivot), nameof(ListFormWizardDto.Tree), nameof(ListFormWizardDto.Chart),
nameof(ListFormWizardDto.Gantt), nameof(ListFormWizardDto.Scheduler), nameof(ListFormWizardDto.Todo),
nameof(ListFormWizardDto.DataSourceCode), nameof(ListFormWizardDto.SelectCommandType), nameof(ListFormWizardDto.SelectCommand),
nameof(ListFormWizardDto.KeyFieldName), nameof(ListFormWizardDto.KeyFieldDbSourceType),
nameof(ListFormWizardDto.TreeOptionDto), nameof(ListFormWizardDto.GanttOptionDto),
nameof(ListFormWizardDto.SchedulerOptionDto), nameof(ListFormWizardDto.TodoOptionDto),
nameof(ListFormWizardDto.WorkflowDto), nameof(ListFormWizardDto.SubForms), nameof(ListFormWizardDto.Widgets),
];
/// <summary>Grup ve alan tanimlarinin <see cref="Fields"/> bolumunde karsiligi olan anahtarlari.</summary>
private static readonly string[] FieldsOwnedGroupKeys =
[
nameof(WizardColumnGroupInputDto.Caption), nameof(WizardColumnGroupInputDto.ColCount),
];
private static readonly string[] FieldsOwnedItemKeys =
[
nameof(WizardColumnItemInputDto.EditorType), nameof(WizardColumnItemInputDto.EditorOptions),
nameof(WizardColumnItemInputDto.EditorScript), nameof(WizardColumnItemInputDto.ColSpan),
nameof(WizardColumnItemInputDto.IsRequired), nameof(WizardColumnItemInputDto.IncludeInEditingForm),
nameof(WizardColumnItemInputDto.DbSourceType), nameof(WizardColumnItemInputDto.LookupDataSourceType),
nameof(WizardColumnItemInputDto.ValueExpr), nameof(WizardColumnItemInputDto.DisplayExpr),
nameof(WizardColumnItemInputDto.LookupQuery),
];
/// <summary>Dosyada `Menu` basligi altinda toplanan wizard alanlari.</summary>
private static readonly string[] MenuKeys =
[
nameof(ListFormWizardDto.CreateMenu), nameof(ListFormWizardDto.MenuCode), nameof(ListFormWizardDto.MenuOrder),
nameof(ListFormWizardDto.MenuUrl), nameof(ListFormWizardDto.MenuIcon), nameof(ListFormWizardDto.MenuParentCode),
nameof(ListFormWizardDto.MenuParentIcon),
];
/// <summary>
/// Dosyada daha okunakli adla yazilan alanlar. Modul, wizard'in ana bilgisidir; menu ve
/// yetkilerin hangi modul altinda filtrelenecegini belirler.
/// </summary>
private static readonly (string Property, string SeedKey)[] RenamedKeys =
[
(nameof(ListFormWizardDto.MenuParentModuleId), "ModuleId"),
(nameof(ListFormWizardDto.ListFormCode), WizardCodeKey),
];
/// <summary>
/// Ekranin kodu wizard'in kimligidir: menu kodu, yetki adlari ve dil anahtarlari bu koddan
/// turer. Dosyada bu nedenle ekran adiyla degil wizard adiyla anilir.
/// </summary>
private const string WizardCodeKey = "WizardCode";
/// <summary>Dosyadaki okuma sirasi: kimlik, kapsam, sonra menu/dil/alan bloklari.</summary>
private static readonly string[] WizardKeyOrder =
[
nameof(ListFormWizardDto.ComponentKind), nameof(ListFormWizardDto.WizardName), WizardCodeKey,
nameof(ListFormWizardDto.SeededAt), nameof(ListFormWizardDto.CustomComponentName),
nameof(ListFormWizardDto.PermissionGroupName), nameof(ListFormWizardDto.PermissionGroupDisplayNameEn),
nameof(ListFormWizardDto.PermissionGroupDisplayNameTr), "ModuleId",
nameof(ListFormWizardDto.DataSourceConnectionString),
MenuSection, LanguageSection, nameof(ListFormWizardDto.Groups),
];
/// <summary>
/// Dosyada `Language` basligi altinda toplanan dil metinleri. ListForm yalnizca dil
/// anahtarini tasir (`Title = "{ListFormCode}.Title"`); metinlerin tek kaynagi burasidir.
/// </summary>
private static readonly string[] LanguageKeys =
[
nameof(ListFormWizardDto.LanguageTextMenuEn), nameof(ListFormWizardDto.LanguageTextMenuTr),
nameof(ListFormWizardDto.LanguageTextTitleEn), nameof(ListFormWizardDto.LanguageTextTitleTr),
nameof(ListFormWizardDto.LanguageTextDescEn), nameof(ListFormWizardDto.LanguageTextDescTr),
nameof(ListFormWizardDto.LanguageTextMenuParentEn), nameof(ListFormWizardDto.LanguageTextMenuParentTr),
];
private const string MenuSection = "Menu";
private const string LanguageSection = "Language";
private static readonly JsonSerializerOptions SeedWriteOptions = new() { WriteIndented = true };
private static readonly JsonSerializerOptions SeedReadOptions = new() { PropertyNameCaseInsensitive = true };
/// <summary>
/// Dosyaya yazilacak JSON'u uretir: once ortama ozgu alanlar temizlenir, sonra wizard
/// bolumunden ekranin sozlesmesinde zaten bulunan alanlar cikarilir. Wizard bolumunde
/// yalnizca ekrandan uretilemeyen bilgiler kalir: menu, yetki, dil metinleri ve alan
/// basliklarinin iki dilli karsiliklari.
/// </summary>
public string ToSeedJson()
{
ClearRuntimeFields();
var node = JsonSerializer.SerializeToNode(this, SeedWriteOptions);
// Custom yolunda ListForm bolumu yoktur; cikarilan alanlarin geri doldurulacagi bir
// kaynak olmadigi icin wizard bolumu oldugu gibi birakilir.
if (ListForm != null && node?[nameof(Wizard)] is JsonObject wizard)
{
foreach (var key in ListFormOwnedWizardKeys)
wizard.Remove(key);
foreach (var group in wizard[nameof(ListFormWizardDto.Groups)] as JsonArray ?? [])
{
if (group is not JsonObject groupObject)
continue;
foreach (var key in FieldsOwnedGroupKeys)
groupObject.Remove(key);
foreach (var item in groupObject[nameof(WizardColumnGroupInputDto.Items)] as JsonArray ?? [])
{
if (item is JsonObject itemObject)
{
foreach (var key in FieldsOwnedItemKeys)
itemObject.Remove(key);
}
}
}
}
if (node?[nameof(Wizard)] is JsonObject wizardNode)
{
// Baglanti cumlesi yalnizca yeni bir veri kaynagi tanimlanirken doludur; bos degeri
// dosyada tasimanin anlami yok.
if (string.IsNullOrWhiteSpace(Wizard?.DataSourceConnectionString))
wizardNode.Remove(nameof(ListFormWizardDto.DataSourceConnectionString));
// Menu uretmek varsayilan davranistir; dosyada yalnizca istisna (menusuz wizard)
// isaretlenir. Menu alanlari menusuz wizardda da kullanildigi icin (yetki kokunu
// veren MenuCode, Custom yolunda rotayi veren MenuUrl) blok kaldirilmaz.
if (Wizard?.CreateMenu != false)
wizardNode.Remove(nameof(ListFormWizardDto.CreateMenu));
foreach (var (property, seedKey) in RenamedKeys)
Rename(wizardNode, property, seedKey);
MoveIntoSection(wizardNode, MenuSection, MenuKeys);
MoveIntoSection(wizardNode, LanguageSection, LanguageKeys);
node[nameof(Wizard)] = Reorder(wizardNode, WizardKeyOrder);
}
return node?.ToJsonString(SeedWriteOptions) ?? string.Empty;
}
/// <summary>
/// Seed dosyasini okur: once `Menu`/`Language` basliklari duzlestirilir (sozlesme duz
/// oldugu icin), sonra wizard bolumu ekranin sozlesmesinden tamamlanir.
/// </summary>
public static WizardSeedFileDto FromSeedJson(string json)
{
var node = JsonNode.Parse(json);
if (node?[nameof(Wizard)] is JsonObject wizardNode)
{
FlattenSection(wizardNode, MenuSection);
FlattenSection(wizardNode, LanguageSection);
foreach (var (property, seedKey) in RenamedKeys)
Rename(wizardNode, seedKey, property);
}
return node.Deserialize<WizardSeedFileDto>(SeedReadOptions)?.Rehydrate();
}
private static void Rename(JsonObject wizard, string from, string to)
{
if (wizard.Remove(from, out var value))
wizard[to] = value;
}
/// <summary>
/// Wizard bolumunu verilen anahtar sirasina gore yeniden kurar; listede olmayan anahtarlar
/// mevcut siralarini koruyarak sona eklenir. JSON nesnesi ekleme sirasini korudugu icin
/// siralamanin tek yolu nesneyi yeniden olusturmaktir.
/// </summary>
private static JsonObject Reorder(JsonObject wizard, string[] keyOrder)
{
var ordered = new JsonObject();
foreach (var key in keyOrder.Concat(wizard.Select(x => x.Key).ToArray()))
{
if (!ordered.ContainsKey(key) && wizard.Remove(key, out var value))
ordered[key] = value;
}
return ordered;
}
/// <summary>Verilen anahtarlari wizard bolumunden alip tek bir baslik altina tasir.</summary>
private static void MoveIntoSection(JsonObject wizard, string section, string[] keys)
{
var target = new JsonObject();
foreach (var key in keys)
{
if (wizard.Remove(key, out var value))
target[key] = value;
}
if (target.Count > 0)
wizard[section] = target;
}
/// <summary>Baslik altindaki alanlari wizard bolumune geri tasir.</summary>
private static void FlattenSection(JsonObject wizard, string section)
{
if (!wizard.Remove(section, out var value) || value is not JsonObject group)
return;
foreach (var key in group.Select(x => x.Key).ToArray())
{
if (group.Remove(key, out var item))
wizard[key] = item;
}
}
/// <summary>
/// Dosyadan okunan wizard bolumunu ekranin sozlesmesinden tamamlar. Dosyada tekrar edilmeyen
/// alanlar (veri kaynagi, duzen, duzenleme izinleri, alan tanimlari) <see cref="ListForm"/> ve
/// <see cref="Fields"/> uzerinden doldurulur; wizard'i okuyan her kod tam nesneyi gorur.
/// </summary>
private WizardSeedFileDto Rehydrate()
{
if (Wizard != null && ListForm != null)
Wizard.RefreshFrom(ListForm, Fields ?? []);
return this;
}
private static void ClearAudit(AuditedEntityDto<Guid> dto)
{
if (dto == null)
return;
dto.Id = default;
dto.CreationTime = default;
dto.CreatorId = null;
dto.LastModificationTime = null;
dto.LastModifierId = null;
}
}
/// <summary>

View file

@ -21,12 +21,16 @@ public class ListFormFieldsAppService : CrudAppService<
{
public IRepository<ListForm, Guid> RepoListForm { get; }
private readonly IWizardSeedSynchronizer wizardSeedSynchronizer;
public ListFormFieldsAppService(
IRepository<ListFormField, Guid> _repository,
IRepository<ListForm, Guid> _repoListForm
IRepository<ListForm, Guid> _repoListForm,
IWizardSeedSynchronizer _wizardSeedSynchronizer
) : base(_repository)
{
RepoListForm = _repoListForm;
wizardSeedSynchronizer = _wizardSeedSynchronizer;
CreatePolicyName = $"{AppCodes.Listforms.ListformField}.Create";
UpdatePolicyName = $"{AppCodes.Listforms.ListformField}.Update";
@ -119,7 +123,12 @@ public class ListFormFieldsAppService : CrudAppService<
});
}
return await base.CreateAsync(input);
var created = await base.CreateAsync(input);
// Ekran wizard ile uretildiyse seed dosyasi da guncel alan listesini tasimalidir.
await wizardSeedSynchronizer.SyncAsync(input.ListFormCode);
return created;
}
public override async Task<ColumnFormatEditDto> UpdateAsync(Guid id, ColumnFormatEditDto input)
@ -214,9 +223,20 @@ public class ListFormFieldsAppService : CrudAppService<
await Repository.UpdateAsync(item, autoSave: true);
await wizardSeedSynchronizer.SyncAsync(item.ListFormCode);
return await MapToGetOutputDtoAsync(item);
}
public override async Task DeleteAsync(Guid id)
{
var listFormCode = (await GetEntityByIdAsync(id)).ListFormCode;
await base.DeleteAsync(id);
await wizardSeedSynchronizer.SyncAsync(listFormCode);
}
[Authorize(AppCodes.Listforms.ListformField + ".Create")]
public async Task<CopyListFormFieldColumnDto> CopyField(CopyListFormFieldColumnDto input)
{
@ -324,6 +344,8 @@ public class ListFormFieldsAppService : CrudAppService<
await Repository.InsertAsync(field);
await wizardSeedSynchronizer.SyncAsync(field.ListFormCode);
return input;
}
}

View file

@ -20,12 +20,15 @@ namespace Sozsoft.Platform.ListForms.Administration;
public class ListFormJsonRowAppService : PlatformAppService
{
private readonly IRepository<ListForm, Guid> repository;
private readonly IWizardSeedSynchronizer wizardSeedSynchronizer;
public ListFormJsonRowAppService(
IRepository<ListForm, Guid> _repository
IRepository<ListForm, Guid> _repository,
IWizardSeedSynchronizer _wizardSeedSynchronizer
) : base()
{
repository = _repository;
wizardSeedSynchronizer = _wizardSeedSynchronizer;
}
public async Task<dynamic> GetAllAsync(Guid id, string field)
@ -183,6 +186,9 @@ public class ListFormJsonRowAppService : PlatformAppService
}
await repository.UpdateAsync(listForm);
// Ekran wizard ile uretildiyse seed dosyasi da guncel halini tasimalidir.
await wizardSeedSynchronizer.SyncAsync(listForm.ListFormCode);
}
public async Task UpdateAsync(Guid id, CrudFieldsDefaultValueJsonItemDto model)
@ -237,6 +243,9 @@ public class ListFormJsonRowAppService : PlatformAppService
}
await repository.UpdateAsync(listForm);
// Ekran wizard ile uretildiyse seed dosyasi da guncel halini tasimalidir.
await wizardSeedSynchronizer.SyncAsync(listForm.ListFormCode);
}
public async Task DeleteAsync(Guid id, string field, int index)
@ -291,6 +300,9 @@ public class ListFormJsonRowAppService : PlatformAppService
}
await repository.UpdateAsync(listForm);
// Ekran wizard ile uretildiyse seed dosyasi da guncel halini tasimalidir.
await wizardSeedSynchronizer.SyncAsync(listForm.ListFormCode);
}
private async Task<ListForm> GetAsync(Guid id)

View file

@ -21,12 +21,15 @@ public class ListFormsAppService : CrudAppService<
GridOptionsEditDto>, IListFormsAppService
{
private readonly IListFormAuthorizationManager authManager;
private readonly IWizardSeedSynchronizer wizardSeedSynchronizer;
public ListFormsAppService(
IRepository<ListForm, Guid> _repository,
IListFormAuthorizationManager _authManager) : base(_repository)
IListFormAuthorizationManager _authManager,
IWizardSeedSynchronizer _wizardSeedSynchronizer) : base(_repository)
{
authManager = _authManager;
wizardSeedSynchronizer = _wizardSeedSynchronizer;
CreatePolicyName = $"{AppCodes.Listforms.Listform}.Create";
UpdatePolicyName = $"{AppCodes.Listforms.Listform}.Update";
@ -239,6 +242,9 @@ public class ListFormsAppService : CrudAppService<
await Repository.UpdateAsync(item, autoSave: true);
// Ekran wizard ile uretildiyse seed dosyasi da guncel halini tasimalidir.
await wizardSeedSynchronizer.SyncAsync(item.ListFormCode);
return await MapToGetOutputDtoAsync(item);
}
}

View file

@ -74,7 +74,6 @@ public class ListFormWizardAppService(
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
private static readonly JsonSerializerOptions SeedJsonReadOptions = new() { PropertyNameCaseInsensitive = true };
private static readonly JsonSerializerOptions SeedJsonWriteOptions = new() { WriteIndented = true };
[UnitOfWork]
public async Task Create(ListFormWizardDto input)
@ -92,18 +91,18 @@ public class ListFormWizardAppService(
: AppCodes.Listforms.Wizards.Create);
if (isEdit)
{
// Ilk deploy zamani wizard'in kimligidir; duzenleme onu degistirmez.
input.SeededAt = await ReadSeededAtAsync(editFileName!);
await DeleteFileInternalAsync(editFileName!);
}
var wizardName = input.WizardName?.Trim();
if (string.IsNullOrWhiteSpace(wizardName))
throw new UserFriendlyException("Wizard adı zorunludur.");
var code = string.IsNullOrWhiteSpace(input.MenuCode)
? WizardConsts.WizardKey(wizardName)
: input.MenuCode.Trim();
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
? code
: input.ListFormCode.Trim();
var code = input.ResolveMenuCode();
var listFormCode = input.ResolveListFormCode();
// Aşağıdaki tüm kayıtlar normalize edilmiş kodlara göre üretilir.
input.ListFormCode = listFormCode;
input.MenuCode = code;
@ -295,7 +294,7 @@ public class ListFormWizardAppService(
if (isCustomComponent)
{
await _languageTextAppService.ClearRedisCacheAsync();
await SaveWizardSeedFileAsync(input, false, false, inserted);
await SaveWizardSeedFileAsync(input, null, [], false, false, inserted);
return;
}
@ -318,21 +317,7 @@ public class ListFormWizardAppService(
inserted.DataSourceCodes.Add(input.DataSourceCode);
}
// Build EditingFormJson from wizard groups (sadece editing form alanı olan gruplar)
var editingFormDtos = input.Groups
.Select((g, gi) => new { Group = g, Order = gi + 1 })
.Where(x => x.Group.Items.Any(i => i.IncludeInEditingForm && i.FieldName != input.KeyFieldName))
.Select(x => new EditingFormDto
{
Order = x.Order,
Caption = x.Group.Caption,
ColCount = x.Group.ColCount,
ColSpan = x.Group.ColCount,
ItemType = "group"
})
.ToList();
//ListForm - varsa sil, yeniden ekle
//ListForm ve alanlari - varsa sil, yeniden uret
var existingListForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == input.ListFormCode);
if (existingListForm != null)
{
@ -354,115 +339,27 @@ public class ListFormWizardAppService(
var tableColumns = await GetTableColumnNamesAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand);
var isDeleted = tableColumns.Contains("IsDeleted");
var isCreated = tableColumns.Contains("CreatorId");
input.WorkflowDto ??= new WorkflowDto();
input.WorkflowDto.Criteria ??= [];
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowDto.Criteria);
NormalizeWorkflowCriteriaIds(listFormCode, input.WorkflowDto.Criteria);
// Ekranin sozlesmesi tek fabrikadan uretilir; seed dosyasina yazilan da veritabanina
// yazilan da ayni nesnedir.
var listFormDto = input.BuildListForm(isDeleted, isCreated);
var fieldDtos = input.BuildFields();
var workflow = input.WorkflowDto;
await repoListForm.InsertAsync(new ListForm
{
ListFormType = ListFormTypeEnum.List,
PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false,
ShowNote = input.SubForms.Count > 0 || workflow.Criteria.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler, input.Todo),
CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode,
Name = nameLangKey,
Title = titleLangKey,
Description = descLangKey,
DataSourceCode = input.DataSourceCode,
IsTenant = input.IsTenant,
IsBranch = input.IsBranch,
IsOrganizationUnit = input.IsOrganizationUnit,
SelectCommandType = input.SelectCommandType,
SelectCommand = input.SelectCommand,
KeyFieldName = input.KeyFieldName,
KeyFieldDbSourceType = input.KeyFieldDbSourceType,
DefaultFilter = isDeleted ? WizardConsts.DefaultFilterJson : null,
SortMode = PlatformConsts.GridOptions.SortModeSingle,
FilterRowJson = WizardConsts.DefaultFilterRowJson(),
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson(),
SearchPanelJson = WizardConsts.DefaultSearchPanelJson(),
GroupPanelJson = WizardConsts.DefaultGroupPanelJson(),
SelectionJson = WizardConsts.DefaultSelectionSingleJson(workflow.Criteria.Count > 0 ? PlatformConsts.GridOptions.SelectionModeSingle : PlatformConsts.GridOptions.SelectionModeNone),
ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(),
PermissionJson = WizardConsts.DefaultPermissionJson(code),
DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
DeleteFieldsDefaultValueJson = isDeleted ? WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultDeleteFieldsJsonOnlyId(input.KeyFieldDbSourceType),
InsertFieldsDefaultValueJson = isCreated ? WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultInsertFieldsJsonOnlyId(input.KeyFieldDbSourceType),
PagerOptionJson = WizardConsts.DefaultPagerOptionJson,
EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, 600, 500, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail),
EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null,
SubFormsJson = input.SubForms.Count > 0 ? JsonSerializer.Serialize(input.SubForms) : null,
WidgetsJson = input.Widgets.Count > 0 ? JsonSerializer.Serialize(input.Widgets) : null,
WorkflowJson = HasWorkflow(workflow) ? JsonSerializer.Serialize(workflow) : null,
TreeOptionJson = (input.Tree || input.DefaultLayout == "tree") &&
!string.IsNullOrEmpty(input.TreeOptionDto?.ParentIdExpr)
? JsonSerializer.Serialize(input.TreeOptionDto)
: null,
GanttOptionJson = (input.Gantt || input.DefaultLayout == "gantt") &&
!string.IsNullOrEmpty(input.GanttOptionDto?.ParentIdExpr)
? JsonSerializer.Serialize(input.GanttOptionDto)
: null,
SchedulerOptionJson = (input.Scheduler || input.DefaultLayout == "scheduler") &&
!string.IsNullOrEmpty(input.SchedulerOptionDto?.TextExpr)
? JsonSerializer.Serialize(input.SchedulerOptionDto)
: null,
TodoOptionJson = (input.Todo || input.DefaultLayout == "todo") &&
!string.IsNullOrEmpty(input.TodoOptionDto?.TitleExpr) &&
!string.IsNullOrEmpty(input.TodoOptionDto?.StatusExpr)
? JsonSerializer.Serialize(input.TodoOptionDto)
: null,
}, autoSave: true);
var listForm = new ListForm();
listFormDto.ApplyTo(listForm);
await repoListForm.InsertAsync(listForm, autoSave: true);
// ListFormField - each item in each group becomes a visible field record
var fieldOrder = 0;
var editGroupOrder = 0;
foreach (var group in input.Groups)
foreach (var fieldDto in fieldDtos)
{
editGroupOrder++;
var editOrder = 0;
foreach (var item in group.Items)
{
fieldOrder++;
if (item.IncludeInEditingForm && item.FieldName != input.KeyFieldName)
{
editOrder++;
}
await repoListFormField.InsertAsync(new ListFormField
{
ListFormCode = input.ListFormCode,
FieldName = item.FieldName,
CaptionName = item.CaptionName,
Visible = item.FieldName != input.KeyFieldName,
IsActive = true,
AllowSearch = true,
Width = 0,
ListOrderNo = fieldOrder,
EditGroupOrderNo = item.IncludeInEditingForm ? editGroupOrder : null,
EditOrderNo = item.IncludeInEditingForm && item.FieldName != input.KeyFieldName
? editOrder
: null,
EditorType2 = item.EditorType,
ColSpan = item.ColSpan,
EditorOptions = string.IsNullOrWhiteSpace(item.EditorOptions) ? null : item.EditorOptions,
EditorScript = string.IsNullOrWhiteSpace(item.EditorScript) ? null : item.EditorScript,
SourceDbType = item.DbSourceType,
CultureName = PlatformConsts.DefaultLanguage,
PermissionJson = WizardConsts.DefaultFieldPermissionJson(code),
ColumnCustomizationJson = WizardConsts.DefaultColumnCustomizationJson,
ColumnFilterJson = WizardConsts.DefaultColumnFilteringJson,
PivotSettingsJson = WizardConsts.DefaultPivotSettingsJson,
LookupJson = !string.IsNullOrWhiteSpace(item.LookupQuery)
? WizardConsts.DefaultLookupJson(item.LookupDataSourceType, item.DisplayExpr, item.ValueExpr, item.LookupQuery)
: null,
}, autoSave: true);
var field = new ListFormField();
fieldDto.ApplyTo(field);
await repoListFormField.InsertAsync(field, autoSave: true);
}
await CreateLangKey(item.CaptionName, item.EnglishCaption, item.TurkishCaption, inserted);
}
foreach (var item in input.Groups.SelectMany(g => g.Items))
{
await CreateLangKey(item.CaptionName, item.EnglishCaption, item.TurkishCaption, inserted);
}
foreach (var criteria in workflow.Criteria)
@ -496,153 +393,48 @@ public class ListFormWizardAppService(
await _languageTextAppService.ClearRedisCacheAsync();
// Wizard konfigürasyonunu seed dosyasına kaydet
await SaveWizardSeedFileAsync(input, isDeleted, isCreated, inserted);
}
private static bool HasWorkflow(WorkflowDto workflow)
{
return workflow != null && (
!string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) ||
workflow.Criteria.Count > 0
);
}
private static void EnsureUniqueWorkflowCriteriaTitles(List<ListFormWorkflowCriteriaDto> criteria)
{
if (criteria == null || criteria.Count == 0)
{
return;
}
var baseTitles = criteria
.Select(x => string.IsNullOrWhiteSpace(x.Title) ? NormalizeWorkflowTitleFallback(x.Kind) : x.Title.Trim())
.ToList();
var duplicateTitles = baseTitles
.GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
.Where(x => x.Count() > 1)
.Select(x => x.Key)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var titleCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var usedTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in criteria)
{
var baseTitle = string.IsNullOrWhiteSpace(item.Title) ? NormalizeWorkflowTitleFallback(item.Kind) : item.Title.Trim();
var title = baseTitle;
if (duplicateTitles.Contains(baseTitle))
{
titleCounts.TryGetValue(baseTitle, out var count);
count++;
titleCounts[baseTitle] = count;
title = $"{baseTitle}{count}";
}
if (usedTitles.Contains(title))
{
var index = 1;
var candidate = $"{title}{index}";
while (usedTitles.Contains(candidate))
{
index++;
candidate = $"{title}{index}";
}
title = candidate;
}
item.Title = title;
usedTitles.Add(title);
}
}
private static string NormalizeWorkflowTitleFallback(string kind)
{
return string.IsNullOrWhiteSpace(kind) ? "Step" : kind.Trim();
}
private static void NormalizeWorkflowCriteriaIds(string listFormCode, List<ListFormWorkflowCriteriaDto> criteria)
{
if (string.IsNullOrWhiteSpace(listFormCode) || criteria == null || criteria.Count == 0)
{
return;
}
foreach (var item in criteria)
{
item.Id = NormalizeWorkflowCriteriaId(listFormCode, item.Id);
item.ListFormCode = listFormCode;
item.NextOnStart = NormalizeWorkflowTargetId(listFormCode, item.NextOnStart);
item.NextOnTrue = NormalizeWorkflowTargetId(listFormCode, item.NextOnTrue);
item.NextOnFalse = NormalizeWorkflowTargetId(listFormCode, item.NextOnFalse);
item.NextOnApprove = NormalizeWorkflowTargetId(listFormCode, item.NextOnApprove);
item.NextOnReject = NormalizeWorkflowTargetId(listFormCode, item.NextOnReject);
foreach (var outcome in item.CompareOutcomes ?? [])
{
outcome.TargetId = NormalizeWorkflowTargetId(listFormCode, outcome.TargetId);
}
}
}
private static string NormalizeWorkflowTargetId(string listFormCode, string id)
{
return string.IsNullOrWhiteSpace(id)
? string.Empty
: NormalizeWorkflowCriteriaId(listFormCode, id);
}
private static string NormalizeWorkflowCriteriaId(string listFormCode, string id)
{
var trimmed = id?.Trim();
if (string.IsNullOrWhiteSpace(trimmed))
{
return string.Empty;
}
if (trimmed.StartsWith($"{listFormCode}-", StringComparison.OrdinalIgnoreCase))
{
return trimmed;
}
return trimmed.StartsWith("N", StringComparison.OrdinalIgnoreCase)
? $"{listFormCode}-{trimmed}"
: trimmed;
await SaveWizardSeedFileAsync(input, listFormDto, fieldDtos, isDeleted, isCreated, inserted);
}
/// <summary>
/// Wizard konfigürasyonunu JSON dosyası olarak kaydeder.
/// Dosya, seeder'lar ve File Manager ile ortak olan seed klasörünün wizard dizinine yazılır.
/// Veritabanı silinip yeniden oluşturulduğunda WizardDataSeeder bu dosyaları okuyarak konfigürasyonu geri yükler.
/// <para>
/// Dosya yalnizca wizard'in cevaplarini degil, urettigi ekranin ListForm sozlesmesini de tasir;
/// tasarimci uzerinde yapilan degisiklikler ayni bolumlere geri yazildigi icin dosya her zaman
/// ekranin guncel halidir.
/// </para>
/// </summary>
private async Task SaveWizardSeedFileAsync(ListFormWizardDto input, bool isDeletedField, bool isCreatedField, WizardInsertedRecordsDto inserted)
private async Task SaveWizardSeedFileAsync(
ListFormWizardDto input,
GridOptionsEditDto listForm,
List<ColumnFormatEditDto> fields,
bool isDeletedField,
bool isCreatedField,
WizardInsertedRecordsDto inserted)
{
try
{
var outputPath = ResolveWizardSeedOutputPath();
Directory.CreateDirectory(outputPath);
// Duzenleme bilgisi yalnizca istege aittir; seed dosyasinda yeri yoktur.
input.EditFileName = null;
// Duzenlemede ilk deploy zamani korunur; seeder dosyalari bu damgaya gore siralar.
if (input.SeededAt == default)
input.SeededAt = Clock.Now;
var seedData = new WizardSeedFileDto
{
Wizard = input,
ListForm = listForm,
Fields = fields ?? [],
IsDeletedField = isDeletedField,
IsCreatedField = isCreatedField,
InsertedRecords = inserted
};
var json = JsonSerializer.Serialize(seedData, SeedJsonWriteOptions);
// Dosya adı kullanıcı girdisinden türetildiği için geçersiz karakterler ayıklanır.
var safeWizardName = string.Concat(input.WizardName.Trim().Split(Path.GetInvalidFileNameChars()));
if (string.IsNullOrWhiteSpace(safeWizardName))
safeWizardName = "Wizard";
var timestamp = Clock.Now.ToString("yyyyMMddHHmmss");
var filePath = Path.Combine(outputPath, $"{timestamp}_{safeWizardName}.json");
await File.WriteAllTextAsync(filePath, json);
var filePath = Path.Combine(outputPath, ResolveWizardSeedFileName(input.WizardName));
await File.WriteAllTextAsync(filePath, seedData.ToSeedJson());
logger.LogInformation("Seed file saved: {FilePath}", filePath);
}
@ -653,6 +445,38 @@ public class ListFormWizardAppService(
}
}
/// <summary>
/// Duzenlenen dosyanin ilk deploy zamanini okur; dosya okunamazsa varsayilan deger doner ve
/// kayit sirasinda yeni damga atilir.
/// </summary>
private async Task<DateTime> ReadSeededAtAsync(string fileName)
{
try
{
var json = await File.ReadAllTextAsync(ResolveWizardSeedFilePath(fileName));
var seed = WizardSeedFileDto.FromSeedJson(json);
return seed?.Wizard?.SeededAt ?? default;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Wizard seed file could not be read for SeededAt: {FileName}", fileName);
return default;
}
}
/// <summary>
/// Seed dosyasinin adi wizard adiyla aynidir; dosya adi tarih tasimaz. Ad kullanici
/// girdisinden turedigi icin gecersiz karakterler ayiklanir.
/// </summary>
private static string ResolveWizardSeedFileName(string wizardName)
{
var safeWizardName = string.Concat((wizardName ?? string.Empty).Trim().Split(Path.GetInvalidFileNameChars()));
if (string.IsNullOrWhiteSpace(safeWizardName))
safeWizardName = "Wizard";
return $"{safeWizardName}.json";
}
/// <summary>
/// Custom yolunda component adı ve rota zorunludur. Rota <see cref="ListFormWizardDto.MenuUrl"/>
/// üzerinden taşınır ve CustomComponent sözleşmesiyle (<c>CreateUpdateCustomComponentDto.RoutePath</c>)
@ -724,7 +548,7 @@ public class ListFormWizardAppService(
try
{
var json = await File.ReadAllTextAsync(file);
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, SeedJsonReadOptions);
var seed = WizardSeedFileDto.FromSeedJson(json);
result.Add(new WizardFileInfoDto
{
FileName = fileName,
@ -735,7 +559,7 @@ public class ListFormWizardAppService(
ComponentKind = seed?.Wizard?.ComponentKind ?? WizardComponentKindEnum.List,
CustomComponentName = seed?.Wizard?.CustomComponentName ?? string.Empty,
MenuUrl = seed?.Wizard?.MenuUrl ?? string.Empty,
CreatedAt = fileName.Length >= 12 ? fileName[..12] : fileName,
CreatedAt = seed?.Wizard?.SeededAt.ToString("s") ?? string.Empty,
HasInsertedRecords = seed?.InsertedRecords != null &&
(seed.InsertedRecords.LanguageKeys.Count > 0 ||
seed.InsertedRecords.PermissionGroupNames.Count > 0 ||
@ -759,7 +583,7 @@ public class ListFormWizardAppService(
var filePath = ResolveWizardSeedFilePath(fileName);
var json = await File.ReadAllTextAsync(filePath);
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, SeedJsonReadOptions);
var seed = WizardSeedFileDto.FromSeedJson(json);
return seed ?? throw new UserFriendlyException("Dosya okunamadı.");
}
@ -779,7 +603,7 @@ public class ListFormWizardAppService(
var filePath = ResolveWizardSeedFilePath(fileName);
var json = await File.ReadAllTextAsync(filePath);
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, SeedJsonReadOptions);
var seed = WizardSeedFileDto.FromSeedJson(json);
if (seed != null)
await DeleteWizardDataAsync(seed);
@ -992,7 +816,7 @@ public class ListFormWizardAppService(
{
var filePath = ResolveWizardSeedFilePath(fileName);
var json = await File.ReadAllTextAsync(filePath);
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, SeedJsonReadOptions)
var seed = WizardSeedFileDto.FromSeedJson(json)
?? throw new UserFriendlyException("Dosya okunamadı.");
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));

View file

@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Entities;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectMapping;
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// ListForm tasarimcisinda yapilan degisiklikleri wizard seed dosyasina geri yazar.
/// </summary>
public interface IWizardSeedSynchronizer
{
/// <summary>
/// Verilen ListForm koduna ait bir wizard seed dosyasi varsa dosyanin ekran ve alan
/// bolumlerini veritabanindaki guncel haliyle degistirir. Dosya yoksa hicbir sey yapmaz.
/// </summary>
Task SyncAsync(string listFormCode);
}
/// <summary>
/// Wizard ile uretilen bir ekran, sonrasinda ListForm detayindan duzenlenebilir. Bu iki yol
/// ayrisirsa wizard yeniden deploy edildiginde tasarimcida yapilan is kaybolur; bu nedenle
/// ListForm ve alan kayitlarinin her degisiminde seed dosyasi da guncellenir.
/// </summary>
public class WizardSeedSynchronizer(
IRepository<ListForm, Guid> repoListForm,
IRepository<ListFormField, Guid> repoListFormField,
IObjectMapper objectMapper,
IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<WizardSeedSynchronizer> logger) : IWizardSeedSynchronizer, ITransientDependency
{
public async Task SyncAsync(string listFormCode)
{
if (string.IsNullOrWhiteSpace(listFormCode))
return;
try
{
var (filePath, seed) = await FindSeedFileAsync(listFormCode);
if (seed == null)
return;
var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
if (listForm == null)
return;
var fields = await repoListFormField.GetListAsync(a => a.ListFormCode == listFormCode);
seed.ListForm = objectMapper.Map<ListForm, GridOptionsEditDto>(listForm);
seed.Fields = [.. fields
.OrderBy(f => f.ListOrderNo)
.Select(objectMapper.Map<ListFormField, ColumnFormatEditDto>)];
// Wizard bolumu de guncellenir; aksi halde ayni wizard yeniden deploy edildiginde
// tasarimcidaki degisiklikler eski cevaplarla ezilirdi.
seed.Wizard.RefreshFrom(seed.ListForm, seed.Fields);
await File.WriteAllTextAsync(filePath, seed.ToSeedJson());
logger.LogInformation("Wizard seed file synchronized: {FilePath}", filePath);
}
catch (Exception ex)
{
// Senkronizasyon hatasi tasarimcidaki kaydetme islemini engellemez.
logger.LogError(ex, "Wizard seed file could not be synchronized for '{ListFormCode}': {Message}", listFormCode, ex.Message);
}
}
/// <summary>
/// Kapsam klasorundeki wizard dosyalari icinde bu ListForm koduna ait olani bulur. Dosya adi
/// wizard adiyla ayni oldugu icin once dogrudan ad denenir; eski ya da elle adlandirilmis
/// dosyalar icin klasor taranir.
/// </summary>
private async Task<(string FilePath, WizardSeedFileDto Seed)> FindSeedFileAsync(string listFormCode)
{
var wizardPath = SeedPathResolver.GetScopePath(configuration, currentTenant.Id, SeedPathResolver.WizardFolder);
if (!Directory.Exists(wizardPath))
return (null, null);
var candidates = new List<string>();
var directPath = Path.Combine(wizardPath, $"{listFormCode}.json");
if (File.Exists(directPath))
candidates.Add(directPath);
candidates.AddRange(Directory
.GetFiles(wizardPath, "*.json")
.Where(f => !string.Equals(f, directPath, StringComparison.OrdinalIgnoreCase)));
foreach (var candidate in candidates)
{
var seed = await ReadSeedAsync(candidate);
if (seed?.Wizard != null &&
string.Equals(seed.Wizard.ListFormCode, listFormCode, StringComparison.OrdinalIgnoreCase))
{
return (candidate, seed);
}
}
return (null, null);
}
private async Task<WizardSeedFileDto> ReadSeedAsync(string filePath)
{
try
{
return WizardSeedFileDto.FromSeedJson(await File.ReadAllTextAsync(filePath));
}
catch (JsonException ex)
{
logger.LogWarning(ex, "Wizard seed file could not be parsed: {FilePath}", filePath);
return null;
}
}
}

View file

@ -17190,6 +17190,18 @@
"en": "Select at least one operation or turn generation off",
"tr": "En az bir operasyon seçin ya da üretimi kapatın"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.ExistingCrudEndpoints",
"en": "Existing CRUD endpoints",
"tr": "Kayıtlı CRUD endpoint'leri"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.ExistingCrudEndpointsHint",
"en": "This table already has generated endpoints; deploying again regenerates them with the operations selected below.",
"tr": "Bu tablo için endpoint'ler daha önce üretilmiş; tekrar deploy edildiğinde aşağıda seçilen operasyonlarla yeniden üretilir."
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.CrudEndpointsInactiveNote",

View file

@ -84,30 +84,44 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
return;
}
var jsonFiles = Directory.GetFiles(wizardDataPath, "*.json").OrderBy(f => Path.GetFileName(f)).ToArray();
var jsonFiles = Directory.GetFiles(wizardDataPath, "*.json");
if (jsonFiles.Length == 0)
{
_logger.LogInformation("No JSON files found in Seeds/{Scope}/wizard directory, skipping.", scopeFolderName);
return;
}
_logger.LogInformation("WizardDataSeeder started. {Count} files to be processed.", jsonFiles.Length);
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
// Dosya adi tarih tasimadigi icin uygulama sirasi dosyanin kendi damgasindan gelir;
// birbirine bagli wizard'lar (once Customers, sonra Tasks) uretildikleri sirayla uygulanir.
var seedFiles = new List<(string FilePath, WizardSeedFileDto Seed)>();
foreach (var filePath in jsonFiles)
{
try
{
var json = await File.ReadAllTextAsync(filePath);
var seedFile = JsonSerializer.Deserialize<WizardSeedFileDto>(json, options);
if (seedFile?.Wizard == null)
var seed = WizardSeedFileDto.FromSeedJson(await File.ReadAllTextAsync(filePath));
if (seed?.Wizard == null)
{
_logger.LogWarning("Invalid file skipped: {FilePath}", filePath);
continue;
}
seedFiles.Add((filePath, seed));
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error - {filePath}: {ex.Message}");
}
}
_logger.LogInformation("WizardDataSeeder started. {Count} files to be processed.", seedFiles.Count);
foreach (var (filePath, seedFile) in seedFiles
.OrderBy(x => x.Seed.Wizard.SeededAt)
.ThenBy(x => Path.GetFileName(x.FilePath), StringComparer.OrdinalIgnoreCase))
{
var fileName = Path.GetFileName(filePath);
try
{
var wizardName = seedFile.Wizard.WizardName?.Trim();
if (string.IsNullOrWhiteSpace(wizardName))
{
@ -115,7 +129,12 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
continue;
}
var fileName = Path.GetFileName(filePath);
var isCustomComponent = seedFile.Wizard.ComponentKind == WizardComponentKindEnum.Custom;
if (!isCustomComponent && seedFile.ListForm == null)
{
_logger.LogWarning("[{File}] ListForm section is missing, skipped.", fileName);
continue;
}
await BackfillMenuParentModuleIdAsync(seedFile.Wizard);
@ -123,10 +142,10 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
// idempotency menü kaydı üzerinden değerlendirilir.
// Menusuz Custom wizard'da ne ListForm ne de menu kaydi olustugu icin
// idempotency okuma permission'i uzerinden degerlendirilir.
var alreadySeeded = seedFile.Wizard.ComponentKind == WizardComponentKindEnum.Custom
var alreadySeeded = isCustomComponent
? seedFile.Wizard.CreateMenu
? await _repoMenu.AnyAsync(a => a.Code == ResolveMenuCode(seedFile.Wizard))
: await _repoPerm.AnyAsync(a => a.Name == ResolveMenuCode(seedFile.Wizard))
? await _repoMenu.AnyAsync(a => a.Code == seedFile.Wizard.ResolveMenuCode())
: await _repoPerm.AnyAsync(a => a.Name == seedFile.Wizard.ResolveMenuCode())
: await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode);
if (alreadySeeded)
@ -149,23 +168,10 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
private async Task ApplyWizardSeedAsync(WizardSeedFileDto seedFile)
{
var input = seedFile.Wizard;
var isDeleted = seedFile.IsDeletedField;
var isCreated = seedFile.IsCreatedField;
input.SubForms ??= new List<SubFormDto>();
input.Widgets ??= new List<WidgetEditDto>();
input.WorkflowDto ??= new WorkflowDto();
input.WorkflowDto.Criteria ??= [];
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowDto.Criteria);
var workflow = input.WorkflowDto;
input.Groups ??= [];
var wizardName = input.WizardName.Trim();
var code = string.IsNullOrWhiteSpace(input.MenuCode)
? WizardConsts.WizardKey(wizardName)
: input.MenuCode.Trim();
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
? code
: input.ListFormCode.Trim();
NormalizeWorkflowCriteriaIds(listFormCode, workflow.Criteria);
var code = input.ResolveMenuCode();
var listFormCode = input.ResolveListFormCode();
var titleLangKey = $"{listFormCode}.Title";
var nameLangKey = code;
var descLangKey = $"{listFormCode}.Desc";
@ -344,20 +350,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
}, autoSave: true);
}
// EditingFormJson
var editingFormDtos = input.Groups
.Select((g, gi) => new EditingFormDto
{
Order = gi + 1,
Caption = g.Caption,
ColCount = g.ColCount,
ColSpan = g.ColCount,
ItemType = "group"
})
.Where((_, index) => input.Groups[index].Items.Any(i => i.IncludeInEditingForm && i.FieldName != input.KeyFieldName))
.ToList();
// ListForm - varsa sil, yeniden ekle
// ListForm ve alanlari - varsa sil, seed dosyasindaki sozlesmeden yeniden kur
var existingListForm = await _repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == input.ListFormCode);
if (existingListForm != null)
{
@ -376,170 +369,26 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
await _repoListFormWorkflow.DeleteManyAsync(existingWorkflowCriteria, autoSave: true);
}
// EditingForm'da required olan alanları tespit et, ValidationRuleJson eklemek için
var editingFormFieldNames = input.Groups
.SelectMany(g => g.Items)
.Where(i => i.IncludeInEditingForm && i.FieldName != input.KeyFieldName)
.Select(i => new { FieldName = i.FieldName, Isrequired = i.IsRequired })
.ToList();
// Ekran, seed dosyasinin tasidigi ListForm sozlesmesinden kurulur; boylece wizard
// sonrasinda tasarimci uzerinde yapilan degisiklikler de geri gelir.
var listForm = new ListForm();
seedFile.ListForm.ApplyTo(listForm);
await _repoListForm.InsertAsync(listForm, autoSave: true);
// Heigth ve Width hesaplama
var groupLayouts = input.Groups
.Select(g =>
{
var items = g.Items
.Where(i => i.IncludeInEditingForm && i.FieldName != input.KeyFieldName)
.ToList();
var columnCount = g.ColCount;
if (columnCount <= 0)
columnCount = 1;
// 3 kolonu geçmesin
columnCount = Math.Min(columnCount, WizardConsts.EditFormMaxColumnCount);
// Örneğin 4 item / 3 kolon = 2 satır
var rowCount = (int)Math.Ceiling(items.Count / (double)columnCount);
return new
{
Group = g,
Items = items,
ColumnCount = columnCount,
RowCount = rowCount
};
})
.ToList();
// Form genelinde kullanılacak kolon sayısı
var formColumnCount = groupLayouts.Any()
? groupLayouts.Max(x => x.ColumnCount)
: 1;
// Width hesabı
var formWidth = (WizardConsts.EditFormMaxWidth / WizardConsts.EditFormMaxColumnCount) * formColumnCount;
// Toplam satır sayısı
var totalRowCount = groupLayouts.Sum(x => x.RowCount);
// Height hesabı
var formHeight = WizardConsts.EditFormHeightDefault + (totalRowCount * WizardConsts.EditFormHeightByRow);
// ListForm
await _repoListForm.InsertAsync(new ListForm
foreach (var fieldDto in seedFile.Fields)
{
ListFormType = ListFormTypeEnum.List,
PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false,
ShowNote = input.SubForms.Count > 0 || workflow.Criteria.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler, input.Todo),
CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode,
Name = nameLangKey,
Title = titleLangKey,
Description = descLangKey,
DataSourceCode = input.DataSourceCode,
IsTenant = input.IsTenant,
IsBranch = input.IsBranch,
IsOrganizationUnit = input.IsOrganizationUnit,
SelectCommandType = input.SelectCommandType,
SelectCommand = input.SelectCommand,
KeyFieldName = input.KeyFieldName,
KeyFieldDbSourceType = input.KeyFieldDbSourceType,
DefaultFilter = isDeleted ? WizardConsts.DefaultFilterJson : null,
SortMode = GridOptions.SortModeSingle,
FilterRowJson = WizardConsts.DefaultFilterRowJson(),
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson(),
SearchPanelJson = WizardConsts.DefaultSearchPanelJson(),
GroupPanelJson = WizardConsts.DefaultGroupPanelJson(),
SelectionJson = WizardConsts.DefaultSelectionSingleJson(workflow.Criteria.Count > 0 ? GridOptions.SelectionModeSingle : GridOptions.SelectionModeNone),
ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(),
PermissionJson = WizardConsts.DefaultPermissionJson(code),
DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
DeleteFieldsDefaultValueJson = isDeleted
? WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType)
: WizardConsts.DefaultDeleteFieldsJsonOnlyId(input.KeyFieldDbSourceType),
InsertFieldsDefaultValueJson = isCreated
? WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType)
: WizardConsts.DefaultInsertFieldsJsonOnlyId(input.KeyFieldDbSourceType),
PagerOptionJson = WizardConsts.DefaultPagerOptionJson,
EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, formWidth, formHeight, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail),
EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null,
SubFormsJson = input.SubForms.Count > 0 ? JsonSerializer.Serialize(input.SubForms) : null,
WidgetsJson = input.Widgets.Count > 0 ? JsonSerializer.Serialize(input.Widgets) : null,
WorkflowJson = HasWorkflow(workflow) ? JsonSerializer.Serialize(workflow) : null,
TreeOptionJson = (input.Tree || input.DefaultLayout == "tree") &&
!string.IsNullOrEmpty(input.TreeOptionDto?.ParentIdExpr)
? JsonSerializer.Serialize(input.TreeOptionDto)
: null,
GanttOptionJson = (input.Gantt || input.DefaultLayout == "gantt") &&
!string.IsNullOrEmpty(input.GanttOptionDto?.ParentIdExpr)
? JsonSerializer.Serialize(input.GanttOptionDto)
: null,
SchedulerOptionJson = (input.Scheduler || input.DefaultLayout == "scheduler") &&
!string.IsNullOrEmpty(input.SchedulerOptionDto?.TextExpr)
? JsonSerializer.Serialize(input.SchedulerOptionDto)
: null,
TodoOptionJson = (input.Todo || input.DefaultLayout == "todo") &&
!string.IsNullOrEmpty(input.TodoOptionDto?.TitleExpr) &&
!string.IsNullOrEmpty(input.TodoOptionDto?.StatusExpr)
? JsonSerializer.Serialize(input.TodoOptionDto)
: null,
}, autoSave: true);
// ListFormFields
var fieldOrder = 0;
var editGroupOrder = 0;
foreach (var group in input.Groups)
{
editGroupOrder++;
var editOrder = 0;
foreach (var item in group.Items)
{
fieldOrder++;
if (item.IncludeInEditingForm && item.FieldName != input.KeyFieldName)
{
editOrder++;
}
await _repoListFormField.InsertAsync(new ListFormField
{
ListFormCode = input.ListFormCode,
FieldName = item.FieldName,
CaptionName = item.CaptionName,
Visible = item.FieldName != input.KeyFieldName,
IsActive = true,
Width = 0, //Fit columns için hepsine sıfır veriyorum.
AllowSearch = true,
ListOrderNo = fieldOrder,
EditGroupOrderNo = item.IncludeInEditingForm ? editGroupOrder : null,
EditOrderNo = item.IncludeInEditingForm && item.FieldName != input.KeyFieldName
? editOrder
: null,
EditorType2 = item.EditorType,
ColSpan = item.ColSpan,
EditorOptions = string.IsNullOrWhiteSpace(item.EditorOptions) ? null : item.EditorOptions,
EditorScript = string.IsNullOrWhiteSpace(item.EditorScript) ? null : item.EditorScript,
SourceDbType = item.DbSourceType,
CultureName = PlatformConsts.DefaultLanguage,
PermissionJson = WizardConsts.DefaultFieldPermissionJson(code),
ColumnCustomizationJson = WizardConsts.DefaultColumnCustomizationJson,
ColumnFilterJson = WizardConsts.DefaultColumnFilteringJson,
PivotSettingsJson = WizardConsts.DefaultPivotSettingsJson,
LookupJson = !string.IsNullOrWhiteSpace(item.LookupQuery)
? WizardConsts.DefaultLookupJson(item.LookupDataSourceType, item.DisplayExpr, item.ValueExpr, item.LookupQuery)
: null,
ValidationRuleJson = editingFormFieldNames.Any(f => f.FieldName == item.FieldName && f.Isrequired)
? WizardConsts.DefaultValidationRuleRequiredJson
: null
}, autoSave: true);
await CreateLangKeyAsync(item.CaptionName, item.EnglishCaption, item.TurkishCaption);
}
var field = new ListFormField();
fieldDto.ApplyTo(field);
await _repoListFormField.InsertAsync(field, autoSave: true);
}
foreach (var criteria in workflow.Criteria)
// Alan basliklarinin iki dilli metinleri yalnizca wizard bolumunde tasinir.
foreach (var item in input.Groups.SelectMany(g => g.Items))
{
await CreateLangKeyAsync(item.CaptionName, item.EnglishCaption, item.TurkishCaption);
}
foreach (var criteria in seedFile.ListForm.WorkflowDto.Criteria)
{
if (string.IsNullOrWhiteSpace(criteria.Id))
{
@ -568,17 +417,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
}
}
/// <summary>
/// Wizard'in menu kodunu, ApplyWizardSeedAsync ile ayni kurala gore cozer.
/// Custom yolunda idempotency kontrolu ListForm yerine bu kod uzerinden yapilir.
/// </summary>
private static string ResolveMenuCode(ListFormWizardDto input)
{
return string.IsNullOrWhiteSpace(input.MenuCode)
? WizardConsts.WizardKey(input.WizardName.Trim())
: input.MenuCode.Trim();
}
/// <summary>
/// Custom yolunda menunun acacagi rotayi normalize eder. Rota MenuUrl uzerinden
/// tasinir; bos ise menu hicbir yere gitmeyecegi icin seed dosyasi hatali kabul edilir.
@ -647,119 +485,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
menuParent.ModuleId);
}
private static bool HasWorkflow(WorkflowDto workflow)
{
return workflow != null && (
!string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalDateFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalDescriptionFieldName) ||
workflow.Criteria.Count > 0
);
}
private static void EnsureUniqueWorkflowCriteriaTitles(List<ListFormWorkflowCriteriaDto> criteria)
{
if (criteria == null || criteria.Count == 0)
{
return;
}
var baseTitles = criteria
.Select(x => string.IsNullOrWhiteSpace(x.Title) ? NormalizeWorkflowTitleFallback(x.Kind) : x.Title.Trim())
.ToList();
var duplicateTitles = baseTitles
.GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
.Where(x => x.Count() > 1)
.Select(x => x.Key)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var titleCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var usedTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in criteria)
{
var baseTitle = string.IsNullOrWhiteSpace(item.Title) ? NormalizeWorkflowTitleFallback(item.Kind) : item.Title.Trim();
var title = baseTitle;
if (duplicateTitles.Contains(baseTitle))
{
titleCounts.TryGetValue(baseTitle, out var count);
count++;
titleCounts[baseTitle] = count;
title = $"{baseTitle}{count}";
}
if (usedTitles.Contains(title))
{
var index = 1;
var candidate = $"{title}{index}";
while (usedTitles.Contains(candidate))
{
index++;
candidate = $"{title}{index}";
}
title = candidate;
}
item.Title = title;
usedTitles.Add(title);
}
}
private static string NormalizeWorkflowTitleFallback(string kind)
{
return string.IsNullOrWhiteSpace(kind) ? "Step" : kind.Trim();
}
private static void NormalizeWorkflowCriteriaIds(string listFormCode, List<ListFormWorkflowCriteriaDto> criteria)
{
if (string.IsNullOrWhiteSpace(listFormCode) || criteria == null || criteria.Count == 0)
{
return;
}
foreach (var item in criteria)
{
item.Id = NormalizeWorkflowCriteriaId(listFormCode, item.Id);
item.ListFormCode = listFormCode;
item.NextOnStart = NormalizeWorkflowTargetId(listFormCode, item.NextOnStart);
item.NextOnTrue = NormalizeWorkflowTargetId(listFormCode, item.NextOnTrue);
item.NextOnFalse = NormalizeWorkflowTargetId(listFormCode, item.NextOnFalse);
item.NextOnApprove = NormalizeWorkflowTargetId(listFormCode, item.NextOnApprove);
item.NextOnReject = NormalizeWorkflowTargetId(listFormCode, item.NextOnReject);
foreach (var outcome in item.CompareOutcomes ?? [])
{
outcome.TargetId = NormalizeWorkflowTargetId(listFormCode, outcome.TargetId);
}
}
}
private static string NormalizeWorkflowTargetId(string listFormCode, string id)
{
return string.IsNullOrWhiteSpace(id)
? string.Empty
: NormalizeWorkflowCriteriaId(listFormCode, id);
}
private static string NormalizeWorkflowCriteriaId(string listFormCode, string id)
{
var trimmed = id?.Trim();
if (string.IsNullOrWhiteSpace(trimmed))
{
return string.Empty;
}
if (trimmed.StartsWith($"{listFormCode}-", StringComparison.OrdinalIgnoreCase))
{
return trimmed;
}
return trimmed.StartsWith("N", StringComparison.OrdinalIgnoreCase)
? $"{listFormCode}-{trimmed}"
: trimmed;
}
private async Task CreateLangKeyAsync(string key, string textEn, string textTr)
{

View file

@ -0,0 +1,157 @@
using System.Data;
using Sozsoft.Platform.Enums;
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Bir ekranin (ListForm) yazilabilir kolon sozlesmesi. <c>ListForm</c> entity'si bu sozlesmeyi
/// uygular; boylece ekrani DTO'dan kuran cevirici, entity'yi tanimayan katmanlarda da (wizard
/// seed dosyasini isleyen DbMigrator gibi) tek bir yerde tutulabilir.
/// </summary>
public interface IListFormArtifact
{
string ListFormCode { get; set; }
string CultureName { get; set; }
string Name { get; set; }
string Title { get; set; }
string Description { get; set; }
string ListFormType { get; set; }
bool IsSubForm { get; set; }
string SubFormsListFormType { get; set; }
bool ShowNote { get; set; }
string SortMode { get; set; }
int PageSize { get; set; }
int? Width { get; set; }
int? Height { get; set; }
bool FullHeight { get; set; }
string UserId { get; set; }
string RoleId { get; set; }
string DataSourceCode { get; set; }
SelectCommandTypeEnum SelectCommandType { get; set; }
string SelectCommand { get; set; }
string TableName { get; set; }
string KeyFieldName { get; set; }
DbType KeyFieldDbSourceType { get; set; }
bool IsTenant { get; set; }
bool IsBranch { get; set; }
bool IsOrganizationUnit { get; set; }
string DefaultFilter { get; set; }
string InsertServiceAddress { get; set; }
string InsertCommand { get; set; }
string InsertBeforeCommand { get; set; }
string InsertAfterCommand { get; set; }
string UpdateServiceAddress { get; set; }
string UpdateCommand { get; set; }
string UpdateBeforeCommand { get; set; }
string UpdateAfterCommand { get; set; }
string DeleteServiceAddress { get; set; }
string DeleteCommand { get; set; }
string DeleteBeforeCommand { get; set; }
string DeleteAfterCommand { get; set; }
string SelectFieldsDefaultValueJson { get; set; }
string InsertFieldsDefaultValueJson { get; set; }
string UpdateFieldsDefaultValueJson { get; set; }
string DeleteFieldsDefaultValueJson { get; set; }
string FormFieldsDefaultValueJson { get; set; }
string LayoutJson { get; set; }
string FilterRowJson { get; set; }
string RowJson { get; set; }
string HeaderFilterJson { get; set; }
string FilterPanelJson { get; set; }
string SearchPanelJson { get; set; }
string GroupPanelJson { get; set; }
string SelectionJson { get; set; }
string ColumnOptionJson { get; set; }
string PivotOptionJson { get; set; }
string TreeOptionJson { get; set; }
string GanttOptionJson { get; set; }
string SchedulerOptionJson { get; set; }
string TodoOptionJson { get; set; }
string PagerOptionJson { get; set; }
string EditingOptionJson { get; set; }
string EditingFormJson { get; set; }
string PermissionJson { get; set; }
string CommandColumnJson { get; set; }
string StateStoringJson { get; set; }
string CustomJsSourcesJson { get; set; }
string CustomStyleSourcesJson { get; set; }
string SubFormsJson { get; set; }
string WidgetsJson { get; set; }
string WorkflowJson { get; set; }
string ExtraFilterJson { get; set; }
string CommonJson { get; set; }
string AdaptiveLayoutJson { get; set; }
string AnimationJson { get; set; }
string AnnotationsJson { get; set; }
string ArgumentAxisJson { get; set; }
string CommonAnnotationsSettingsJson { get; set; }
string CommonAxisSettingsJson { get; set; }
string CommonPaneSettingsJson { get; set; }
string CommonSeriesSettingsJson { get; set; }
string CrosshairJson { get; set; }
string ExportJson { get; set; }
string LegendJson { get; set; }
string MarginJson { get; set; }
string PanesJson { get; set; }
string ScrollBarJson { get; set; }
string SeriesJson { get; set; }
string SizeJson { get; set; }
string TitleJson { get; set; }
string TooltipJson { get; set; }
string ValueAxisJson { get; set; }
string ZoomAndPanJson { get; set; }
}
/// <summary>
/// Bir ekran alaninin (ListFormField) yazilabilir kolon sozlesmesi;
/// <see cref="IListFormArtifact"/> ile ayni amaca hizmet eder.
/// </summary>
public interface IListFormFieldArtifact
{
string ListFormCode { get; set; }
string UserId { get; set; }
string RoleId { get; set; }
string CultureName { get; set; }
string FieldName { get; set; }
string CaptionName { get; set; }
string PlaceHolder { get; set; }
string BandName { get; set; }
bool? Visible { get; set; }
bool? IsActive { get; set; }
int? Width { get; set; }
int? ListOrderNo { get; set; }
DbType SourceDbType { get; set; }
int? SortIndex { get; set; }
string SortDirection { get; set; }
bool? AllowSearch { get; set; }
bool? AllowEditing { get; set; }
bool? AllowAdding { get; set; }
string Alignment { get; set; }
string Format { get; set; }
int? EditOrderNo { get; set; }
int? EditGroupOrderNo { get; set; }
string EditorType2 { get; set; }
int? ColSpan { get; set; }
string EditorOptions { get; set; }
string EditorScript { get; set; }
string ColumnCssClass { get; set; }
string ColumnCssValue { get; set; }
string ColumnFilterJson { get; set; }
string ColumnHeaderJson { get; set; }
string GroupingJson { get; set; }
string ColumnCustomizationJson { get; set; }
string TotalSummaryJson { get; set; }
string GroupSummaryJson { get; set; }
string JoinTableJson { get; set; }
string LookupJson { get; set; }
string ValidationRuleJson { get; set; }
string ColumnStylingJson { get; set; }
string PermissionJson { get; set; }
string PivotSettingsJson { get; set; }
}

View file

@ -1,11 +1,12 @@
using System;
using System.Data;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms;
using Volo.Abp.Domain.Entities;
namespace Sozsoft.Platform.Entities;
public class ListForm : Entity<Guid>
public class ListForm : Entity<Guid>, IListFormArtifact
{
public string ListFormCode { get; set; } // Guid olan ID'den ayri olarak, bir listform'un kullanici, rol ve dil bazindaki ozellestirmelerinin ana kayit ile baglantisini kurmak icin
public string CultureName { get; set; } // Bu tanim hangi dil icin "en", "tr"

View file

@ -1,11 +1,12 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using Sozsoft.Platform.ListForms;
using Volo.Abp.Domain.Entities;
namespace Sozsoft.Platform.Entities;
public class ListFormField : Entity<Guid>
public class ListFormField : Entity<Guid>, IListFormFieldArtifact
{
public string ListFormCode { get; set; }// Sozsoft.LF.SATIS-323
public string UserId { get; set; } // External kullanici id (orn: ali.akman. ihtiyaca gore guid veya int de olabilir)

View file

@ -156,6 +156,30 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
- **`scriptRecipes.ts` (TS) ↔ `Domain.Shared/Editors/*.cs`** — biri değişirse diğeri de değişir.
İkisinin çıktısı **birebir aynı** olmak zorundadır; aksi hâlde seeder ile basılan script
dialogda "elle düzenlenmiş" sayılır ve kural editörü kapanır.
- **Wizard seed dosyasında tekrar yok**`Wizard` bölümü yalnızca ekrandan üretilemeyen bilgileri
taşır. `ListForm`/`Fields` içinde karşılığı olan her alan (veri kaynağı, select, anahtar alan,
kırılım/düzen/düzenleme bayrakları, görünüm option blokları, alt form, widget, iş akışı, alan
tanımları) dosyaya yazılmadan çıkarılır (`WizardSeedFileDto.ToSeedJson`) ve okurken geri
doldurulur (`FromSeedJson`). Wizard'a yeni bir alan eklerken karşılığı ListForm'da varsa listeye ekle.
Dosyada kalan wizard alanları üç başlık altında toplanır — `Menu`, `Language`, `Groups` — ve
`ListFormCode``WizardCode`, `MenuParentModuleId``ModuleId` adıyla yazılır; varsayılanı olan
alanlar (`CreateMenu: true`, boş `DataSourceConnectionString`) hiç yazılmaz. Bu yalnızca dosya biçimidir; C#
sözleşmesi düzdür, dönüşüm `WizardSeedFileDto.ToSeedJson` / `FromSeedJson` içindedir. **Wizard
seed dosyası her zaman bu iki metotla okunup yazılır**; düz `JsonSerializer` çağrısı bloklu
bölümleri sessizce düşürür.
- **Wizard ile üretilen ekran** → seed dosyası (`configs/seeds/{kapsam}/wizard/{WizardName}.json`)
wizard cevaplarının yanında ekranın `GridOptionsEditDto` (`ListForm`) ve alanların
`ColumnFormatEditDto` (`Fields`) sözleşmesini taşır. ListForm detayından yapılan her kayıt
(`ListFormsAppService`, `ListFormFieldsAppService`, `ListFormJsonRowAppService`) bu bölümleri
ve `Wizard` bölümündeki karşılıklarını geri yazar (`WizardSeedSynchronizer`). Bu üçlüden birine
yeni bir kayıt yolu eklenirse senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar.
- **Wizard girdisinden ekran üretimi** → tek kaynak `ListFormWizardDto.BuildListForm/BuildFields`,
entity'ye yazma `GridOptionsEditDto.ApplyTo` / `ColumnFormatEditDto.ApplyTo`. Deploy, seeder ve
senkronizasyon aynı metotları çağırır; ikinci bir üretim kopyası yazma.
- **Katman sınırı (DDD)**`DbMigrator` Application'a referans vermez. Seeder ile AppService'in
paylaştığı dönüşüm `Application.Contracts`'ta DTO metodudur; entity'ye yazma Domain.Shared'deki
`IListFormArtifact` / `IListFormFieldArtifact` sözleşmeleri üzerinden yapılır.
Ayrıntı: `dotnet.instructions.md` §5.
- **Yeni bir yetki**`PlatformConsts.AppCodes` + `PermissionsData.json` + (UI'da kullanılacaksa)
`permission.constant.ts` + `LanguagesData.json`.
- **Yeni bir dil anahtarı**`LanguagesData.json`; eklemeden önce anahtarın zaten var olup

View file

@ -0,0 +1,36 @@
{
"EntityName": "Sal_D_Appoval",
"GeneratedAt": "2026-09-02T08:17:19.9301653Z",
"Endpoints": [
{
"Method": "POST",
"Path": "/api/app/crudendpoint/Sal_D_Appoval",
"OperationType": "Create",
"IsActive": true
},
{
"Method": "DELETE",
"Path": "/api/app/crudendpoint/Sal_D_Appoval/{id}",
"OperationType": "Delete",
"IsActive": true
},
{
"Method": "GET",
"Path": "/api/app/crudendpoint/Sal_D_Appoval/{id}",
"OperationType": "GetById",
"IsActive": true
},
{
"Method": "GET",
"Path": "/api/app/crudendpoint/Sal_D_Appoval",
"OperationType": "GetList",
"IsActive": true
},
{
"Method": "PUT",
"Path": "/api/app/crudendpoint/Sal_D_Appoval/{id}",
"OperationType": "Update",
"IsActive": true
}
]
}

View file

@ -0,0 +1,17 @@
IF OBJECT_ID(N'[dbo].[Sal_D_Appoval]', 'U') IS NULL
BEGIN
CREATE TABLE [dbo].[Sal_D_Appoval]
(
[Id] uniqueidentifier NOT NULL,
[UserName] nvarchar(512) NULL,
[Status] nvarchar(100) NULL,
[Date] datetime NULL,
[Description] nvarchar(400) NULL,
[Text] nvarchar(100) NULL,
CONSTRAINT [PK_Sal_D_Appoval] PRIMARY KEY NONCLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO

View file

@ -1,240 +0,0 @@
{
"Wizard": {
"ComponentKind": 0,
"WizardName": "TaskTimesheets",
"ListFormCode": "App.Prj.TaskTimesheets",
"MenuCode": "App.Prj.TaskTimesheets",
"MenuOrder": 1,
"CreateMenu": false,
"MenuUrl": "/admin/list/App.Prj.TaskTimesheets",
"CustomComponentName": "",
"IsTenant": true,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": false,
"ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true,
"Card": false,
"Pivot": false,
"Chart": false,
"Tree": false,
"Gantt": false,
"Scheduler": false,
"Todo": false,
"LanguageTextMenuEn": "Timesheets",
"LanguageTextMenuTr": "Çalışma Çizelgeleri",
"LanguageTextTitleEn": "Timesheets",
"LanguageTextTitleTr": "Çalışma Çizelgeleri",
"LanguageTextDescEn": "Task timesheets",
"LanguageTextDescTr": "Görev çalışma çizelgeleri",
"LanguageTextMenuParentEn": "Project",
"LanguageTextMenuParentTr": "Proje",
"PermissionGroupName": "App.Prj",
"PermissionGroupDisplayNameEn": "Project",
"PermissionGroupDisplayNameTr": "Proje",
"MenuParentCode": "App.Prj",
"MenuParentModuleId": "Prj",
"MenuParentIcon": "FcTimeline",
"MenuIcon": "FcTodoList",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Prj_T_Timesheet",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": null,
"StartExpr": null,
"EndExpr": null,
"ProgressExpr": null
},
"GanttOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": "",
"ScaleType": "weeks",
"StartExpr": "",
"EndExpr": "",
"ProgressExpr": "",
"AllowEditing": false,
"AllowTaskAdding": false,
"AllowTaskUpdating": false,
"AllowTaskResourceUpdating": false,
"AllowTaskDeleting": false,
"AllowDependencyAdding": false,
"AllowDependencyDeleting": false,
"AllowResourceAdding": false,
"AllowResourceUpdating": false,
"AllowResourceDeleting": false
},
"SchedulerOptionDto": {
"TextExpr": "",
"UserNameExpr": null,
"DescriptionExpr": null,
"StartDateExpr": "",
"EndDateExpr": "",
"AllDayExpr": "",
"RecurrenceRuleExpr": "",
"RecurrenceExceptionExpr": "",
"StartDayHour": 8,
"EndDayHour": 20,
"DefaultView": "week",
"ShowAllDayPanel": true,
"CellDuration": 30,
"FirstDayOfWeek": 1,
"CrossScrollingEnabled": false,
"AllowResizing": false,
"AllowDragging": false,
"AllowDeleting": false,
"AllowEditing": false,
"AllowAdding": false
},
"TodoOptionDto": {
"TitleExpr": "",
"StatusExpr": "",
"DescriptionExpr": "",
"DueDateExpr": "",
"TagExpr": "",
"AssigneeExpr": "",
"PriorityExpr": "",
"CompletedExpr": "",
"OrderExpr": "",
"StatusOrder": "",
"AllowDragging": true
},
"WorkflowDto": {
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": false,
"Criteria": []
},
"Groups": [
{
"Caption": "Genel",
"ColCount": 2,
"Items": [
{
"FieldName": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"TurkishCaption": "Kimlik",
"EnglishCaption": "Id",
"EditorType": "dxTextBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": false,
"ColSpan": 1
},
{
"FieldName": "TaskId",
"CaptionName": "App.Listform.ListformField.TaskId",
"TurkishCaption": "Görev",
"EnglishCaption": "Task",
"EditorType": "dxSelectBox",
"DbSourceType": 9,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 1,
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], Name AS [Name] FROM dbo.Prj_T_Task WHERE IsDeleted = 0 ORDER BY Name",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "WorkDate",
"CaptionName": "App.Listform.ListformField.WorkDate",
"TurkishCaption": "Tarih",
"EnglishCaption": "Date",
"EditorType": "dxDateBox",
"DbSourceType": 6,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"type\": \"date\", \"displayFormat\": \"dd.MM.yyyy\", \"useMaskBehavior\": true}"
},
{
"FieldName": "UserId",
"CaptionName": "App.Listform.ListformField.UserId",
"TurkishCaption": "Kullanıcı",
"EnglishCaption": "User",
"EditorType": "dxSelectBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], UserName AS [Name] FROM dbo.AbpUsers WHERE IsDeleted = 0 ORDER BY UserName",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "Hours",
"CaptionName": "App.Listform.ListformField.Hours",
"TurkishCaption": "Süre (saat)",
"EnglishCaption": "Hours",
"EditorType": "dxNumberBox",
"DbSourceType": 7,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"format\": {\"type\": \"fixedPoint\", \"precision\": 2}}"
},
{
"FieldName": "Description",
"CaptionName": "App.Listform.ListformField.Description",
"TurkishCaption": "Açıklama",
"EnglishCaption": "Description",
"EditorType": "dxTextArea",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 2,
"EditorOptions": "{\"autoResizeEnabled\": true, \"maxLength\": 500}"
}
]
}
],
"SubForms": [],
"Widgets": []
},
"IsDeletedField": true,
"IsCreatedField": true,
"InsertedRecords": {
"LanguageKeys": [
"App.Prj.TaskTimesheets",
"App.Prj"
],
"PermissionGroupNames": [
"App.Prj"
],
"PermissionNames": [
"App.Prj.TaskTimesheets",
"App.Prj.TaskTimesheets.Create",
"App.Prj.TaskTimesheets.Update",
"App.Prj.TaskTimesheets.Delete",
"App.Prj.TaskTimesheets.Export",
"App.Prj.TaskTimesheets.Import",
"App.Prj.TaskTimesheets.Note"
],
"MenuCodes": [],
"DataSourceCodes": []
}
}

View file

@ -1,265 +0,0 @@
{
"Wizard": {
"ComponentKind": 0,
"WizardName": "TaskSubTasks",
"ListFormCode": "App.Prj.TaskSubTasks",
"MenuCode": "App.Prj.TaskSubTasks",
"MenuOrder": 1,
"CreateMenu": false,
"MenuUrl": "/admin/list/App.Prj.TaskSubTasks",
"CustomComponentName": "",
"IsTenant": true,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": false,
"ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true,
"Card": false,
"Pivot": false,
"Chart": false,
"Tree": false,
"Gantt": false,
"Scheduler": false,
"Todo": false,
"LanguageTextMenuEn": "Sub Tasks",
"LanguageTextMenuTr": "Alt Görevler",
"LanguageTextTitleEn": "Sub Tasks",
"LanguageTextTitleTr": "Alt Görevler",
"LanguageTextDescEn": "Sub task list",
"LanguageTextDescTr": "Alt görev listesi",
"LanguageTextMenuParentEn": "Project",
"LanguageTextMenuParentTr": "Proje",
"PermissionGroupName": "App.Prj",
"PermissionGroupDisplayNameEn": "Project",
"PermissionGroupDisplayNameTr": "Proje",
"MenuParentCode": "App.Prj",
"MenuParentModuleId": "Prj",
"MenuParentIcon": "FcTimeline",
"MenuIcon": "FcTodoList",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Prj_T_Task",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": null,
"StartExpr": null,
"EndExpr": null,
"ProgressExpr": null
},
"GanttOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": "",
"ScaleType": "weeks",
"StartExpr": "",
"EndExpr": "",
"ProgressExpr": "",
"AllowEditing": false,
"AllowTaskAdding": false,
"AllowTaskUpdating": false,
"AllowTaskResourceUpdating": false,
"AllowTaskDeleting": false,
"AllowDependencyAdding": false,
"AllowDependencyDeleting": false,
"AllowResourceAdding": false,
"AllowResourceUpdating": false,
"AllowResourceDeleting": false
},
"SchedulerOptionDto": {
"TextExpr": "",
"UserNameExpr": null,
"DescriptionExpr": null,
"StartDateExpr": "",
"EndDateExpr": "",
"AllDayExpr": "",
"RecurrenceRuleExpr": "",
"RecurrenceExceptionExpr": "",
"StartDayHour": 8,
"EndDayHour": 20,
"DefaultView": "week",
"ShowAllDayPanel": true,
"CellDuration": 30,
"FirstDayOfWeek": 1,
"CrossScrollingEnabled": false,
"AllowResizing": false,
"AllowDragging": false,
"AllowDeleting": false,
"AllowEditing": false,
"AllowAdding": false
},
"TodoOptionDto": {
"TitleExpr": "",
"StatusExpr": "",
"DescriptionExpr": "",
"DueDateExpr": "",
"TagExpr": "",
"AssigneeExpr": "",
"PriorityExpr": "",
"CompletedExpr": "",
"OrderExpr": "",
"StatusOrder": "",
"AllowDragging": true
},
"WorkflowDto": {
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": false,
"Criteria": []
},
"Groups": [
{
"Caption": "Genel",
"ColCount": 2,
"Items": [
{
"FieldName": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"TurkishCaption": "Kimlik",
"EnglishCaption": "Id",
"EditorType": "dxTextBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": false,
"ColSpan": 1
},
{
"FieldName": "Name",
"CaptionName": "App.Listform.ListformField.Name",
"TurkishCaption": "Görev Adı",
"EnglishCaption": "Task Name",
"EditorType": "dxTextBox",
"DbSourceType": 16,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 2,
"EditorOptions": "{\"maxLength\": 300, \"showClearButton\": true}"
},
{
"FieldName": "ParentTaskId",
"CaptionName": "App.Listform.ListformField.ParentTaskId",
"TurkishCaption": "Üst Görev",
"EnglishCaption": "Parent Task",
"EditorType": "dxSelectBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], Name AS [Name] FROM dbo.Prj_T_Task WHERE IsDeleted = 0 ORDER BY Name",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "Status",
"CaptionName": "App.Listform.ListformField.Status",
"TurkishCaption": "Durum",
"EnglishCaption": "Status",
"EditorType": "dxSelectBox",
"DbSourceType": 16,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 1,
"LookupDataSourceType": 1,
"LookupQuery": "[{\"key\": \"Taslak\", \"name\": \"Taslak\"}, {\"key\": \"Devam Ediyor\", \"name\": \"Devam Ediyor\"}, {\"key\": \"Tamamlandı\", \"name\": \"Tamamlandı\"}, {\"key\": \"İptal\", \"name\": \"İptal\"}]",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "AssigneeIds",
"CaptionName": "App.Listform.ListformField.AssigneeIds",
"TurkishCaption": "Atananlar",
"EnglishCaption": "Assignees",
"EditorType": "dxTagBox",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"showSelectionControls\": true, \"maxDisplayedTags\": 3, \"searchEnabled\": true, \"multiline\": true}",
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], UserName AS [Name] FROM dbo.AbpUsers WHERE IsDeleted = 0 ORDER BY UserName",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "ProgressPercent",
"CaptionName": "App.Listform.ListformField.ProgressPercent",
"TurkishCaption": "İlerleme (%)",
"EnglishCaption": "Progress (%)",
"EditorType": "dxNumberBox",
"DbSourceType": 7,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"format\": {\"type\": \"fixedPoint\", \"precision\": 2}, \"min\": 0, \"max\": 100}"
},
{
"FieldName": "PlannedStartDate",
"CaptionName": "App.Listform.ListformField.PlannedStartDate",
"TurkishCaption": "Planlanan Başlangıç",
"EnglishCaption": "Planned Start",
"EditorType": "dxDateBox",
"DbSourceType": 6,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"type\": \"datetime\", \"displayFormat\": \"dd.MM.yyyy HH:mm\", \"useMaskBehavior\": true}"
},
{
"FieldName": "PlannedEndDate",
"CaptionName": "App.Listform.ListformField.PlannedEndDate",
"TurkishCaption": "Planlanan Bitiş",
"EnglishCaption": "Planned End",
"EditorType": "dxDateBox",
"DbSourceType": 6,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"type\": \"datetime\", \"displayFormat\": \"dd.MM.yyyy HH:mm\", \"useMaskBehavior\": true}"
}
]
}
],
"SubForms": [],
"Widgets": []
},
"IsDeletedField": true,
"IsCreatedField": true,
"InsertedRecords": {
"LanguageKeys": [
"App.Prj.TaskSubTasks"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Prj.TaskSubTasks",
"App.Prj.TaskSubTasks.Create",
"App.Prj.TaskSubTasks.Update",
"App.Prj.TaskSubTasks.Delete",
"App.Prj.TaskSubTasks.Export",
"App.Prj.TaskSubTasks.Import",
"App.Prj.TaskSubTasks.Note"
],
"MenuCodes": [],
"DataSourceCodes": []
}
}

View file

@ -1,213 +0,0 @@
{
"Wizard": {
"ComponentKind": 0,
"WizardName": "TaskDependencies",
"ListFormCode": "App.Prj.TaskDependencies",
"MenuCode": "App.Prj.TaskDependencies",
"MenuOrder": 1,
"CreateMenu": false,
"MenuUrl": "/admin/list/App.Prj.TaskDependencies",
"CustomComponentName": "",
"IsTenant": true,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": false,
"ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true,
"Card": false,
"Pivot": false,
"Chart": false,
"Tree": false,
"Gantt": false,
"Scheduler": false,
"Todo": false,
"LanguageTextMenuEn": "Blocking Tasks",
"LanguageTextMenuTr": "Beklenen Görevler",
"LanguageTextTitleEn": "Blocking Tasks",
"LanguageTextTitleTr": "Beklenen Görevler",
"LanguageTextDescEn": "Tasks this task waits for",
"LanguageTextDescTr": "Görevin beklediği görevler",
"LanguageTextMenuParentEn": "Project",
"LanguageTextMenuParentTr": "Proje",
"PermissionGroupName": "App.Prj",
"PermissionGroupDisplayNameEn": "Project",
"PermissionGroupDisplayNameTr": "Proje",
"MenuParentCode": "App.Prj",
"MenuParentModuleId": "Prj",
"MenuParentIcon": "FcTimeline",
"MenuIcon": "FcTodoList",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Prj_T_TaskDependency",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": null,
"StartExpr": null,
"EndExpr": null,
"ProgressExpr": null
},
"GanttOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": "",
"ScaleType": "weeks",
"StartExpr": "",
"EndExpr": "",
"ProgressExpr": "",
"AllowEditing": false,
"AllowTaskAdding": false,
"AllowTaskUpdating": false,
"AllowTaskResourceUpdating": false,
"AllowTaskDeleting": false,
"AllowDependencyAdding": false,
"AllowDependencyDeleting": false,
"AllowResourceAdding": false,
"AllowResourceUpdating": false,
"AllowResourceDeleting": false
},
"SchedulerOptionDto": {
"TextExpr": "",
"UserNameExpr": null,
"DescriptionExpr": null,
"StartDateExpr": "",
"EndDateExpr": "",
"AllDayExpr": "",
"RecurrenceRuleExpr": "",
"RecurrenceExceptionExpr": "",
"StartDayHour": 8,
"EndDayHour": 20,
"DefaultView": "week",
"ShowAllDayPanel": true,
"CellDuration": 30,
"FirstDayOfWeek": 1,
"CrossScrollingEnabled": false,
"AllowResizing": false,
"AllowDragging": false,
"AllowDeleting": false,
"AllowEditing": false,
"AllowAdding": false
},
"TodoOptionDto": {
"TitleExpr": "",
"StatusExpr": "",
"DescriptionExpr": "",
"DueDateExpr": "",
"TagExpr": "",
"AssigneeExpr": "",
"PriorityExpr": "",
"CompletedExpr": "",
"OrderExpr": "",
"StatusOrder": "",
"AllowDragging": true
},
"WorkflowDto": {
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": false,
"Criteria": []
},
"Groups": [
{
"Caption": "Genel",
"ColCount": 2,
"Items": [
{
"FieldName": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"TurkishCaption": "Kimlik",
"EnglishCaption": "Id",
"EditorType": "dxTextBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": false,
"ColSpan": 1
},
{
"FieldName": "TaskId",
"CaptionName": "App.Listform.ListformField.TaskId",
"TurkishCaption": "Görev",
"EnglishCaption": "Task",
"EditorType": "dxSelectBox",
"DbSourceType": 9,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 1,
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], Name AS [Name] FROM dbo.Prj_T_Task WHERE IsDeleted = 0 ORDER BY Name",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "DependsOnTaskId",
"CaptionName": "App.Listform.ListformField.DependsOnTaskId",
"TurkishCaption": "Beklenen Görev",
"EnglishCaption": "Blocking Task",
"EditorType": "dxSelectBox",
"DbSourceType": 9,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 1,
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], Name AS [Name] FROM dbo.Prj_T_Task WHERE IsDeleted = 0 ORDER BY Name",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "Note",
"CaptionName": "App.Listform.ListformField.Note",
"TurkishCaption": "Not",
"EnglishCaption": "Note",
"EditorType": "dxTextArea",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 2,
"EditorOptions": "{\"autoResizeEnabled\": true, \"maxLength\": 300}"
}
]
}
],
"SubForms": [],
"Widgets": []
},
"IsDeletedField": true,
"IsCreatedField": true,
"InsertedRecords": {
"LanguageKeys": [
"App.Prj.TaskDependencies"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Prj.TaskDependencies",
"App.Prj.TaskDependencies.Create",
"App.Prj.TaskDependencies.Update",
"App.Prj.TaskDependencies.Delete",
"App.Prj.TaskDependencies.Export",
"App.Prj.TaskDependencies.Import",
"App.Prj.TaskDependencies.Note"
],
"MenuCodes": [],
"DataSourceCodes": []
}
}

View file

@ -1,197 +0,0 @@
{
"Wizard": {
"ComponentKind": 0,
"WizardName": "Tags",
"ListFormCode": "App.Prj.Tags",
"MenuCode": "App.Prj.Tags",
"MenuOrder": 2,
"CreateMenu": true,
"MenuUrl": "/admin/list/App.Prj.Tags",
"CustomComponentName": "",
"IsTenant": true,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": false,
"ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true,
"Card": false,
"Pivot": false,
"Chart": false,
"Tree": false,
"Gantt": false,
"Scheduler": false,
"Todo": false,
"LanguageTextMenuEn": "Tags",
"LanguageTextMenuTr": "Etiketler",
"LanguageTextTitleEn": "Tags",
"LanguageTextTitleTr": "Etiketler",
"LanguageTextDescEn": "Task tags",
"LanguageTextDescTr": "Görev etiketleri",
"LanguageTextMenuParentEn": "Project",
"LanguageTextMenuParentTr": "Proje",
"PermissionGroupName": "App.Prj",
"PermissionGroupDisplayNameEn": "Project",
"PermissionGroupDisplayNameTr": "Proje",
"MenuParentCode": "App.Prj",
"MenuParentModuleId": "Prj",
"MenuParentIcon": "FcTimeline",
"MenuIcon": "FcRules",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Prj_T_Tag",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": null,
"StartExpr": null,
"EndExpr": null,
"ProgressExpr": null
},
"GanttOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": "",
"ScaleType": "weeks",
"StartExpr": "",
"EndExpr": "",
"ProgressExpr": "",
"AllowEditing": false,
"AllowTaskAdding": false,
"AllowTaskUpdating": false,
"AllowTaskResourceUpdating": false,
"AllowTaskDeleting": false,
"AllowDependencyAdding": false,
"AllowDependencyDeleting": false,
"AllowResourceAdding": false,
"AllowResourceUpdating": false,
"AllowResourceDeleting": false
},
"SchedulerOptionDto": {
"TextExpr": "",
"UserNameExpr": null,
"DescriptionExpr": null,
"StartDateExpr": "",
"EndDateExpr": "",
"AllDayExpr": "",
"RecurrenceRuleExpr": "",
"RecurrenceExceptionExpr": "",
"StartDayHour": 8,
"EndDayHour": 20,
"DefaultView": "week",
"ShowAllDayPanel": true,
"CellDuration": 30,
"FirstDayOfWeek": 1,
"CrossScrollingEnabled": false,
"AllowResizing": false,
"AllowDragging": false,
"AllowDeleting": false,
"AllowEditing": false,
"AllowAdding": false
},
"TodoOptionDto": {
"TitleExpr": "",
"StatusExpr": "",
"DescriptionExpr": "",
"DueDateExpr": "",
"TagExpr": "",
"AssigneeExpr": "",
"PriorityExpr": "",
"CompletedExpr": "",
"OrderExpr": "",
"StatusOrder": "",
"AllowDragging": true
},
"WorkflowDto": {
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": false,
"Criteria": []
},
"Groups": [
{
"Caption": "Genel",
"ColCount": 2,
"Items": [
{
"FieldName": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"TurkishCaption": "Kimlik",
"EnglishCaption": "Id",
"EditorType": "dxTextBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": false,
"ColSpan": 1
},
{
"FieldName": "Name",
"CaptionName": "App.Listform.ListformField.Name",
"TurkishCaption": "Etiket Adı",
"EnglishCaption": "Tag Name",
"EditorType": "dxTextBox",
"DbSourceType": 16,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"maxLength\": 200, \"showClearButton\": true}"
},
{
"FieldName": "ColorCode",
"CaptionName": "App.Listform.ListformField.ColorCode",
"TurkishCaption": "Renk",
"EnglishCaption": "Color",
"EditorType": "dxColorBox",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1
}
]
}
],
"SubForms": [],
"Widgets": []
},
"IsDeletedField": true,
"IsCreatedField": true,
"InsertedRecords": {
"LanguageKeys": [
"App.Prj.Tags"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Prj.Tags",
"App.Prj.Tags.Create",
"App.Prj.Tags.Update",
"App.Prj.Tags.Delete",
"App.Prj.Tags.Export",
"App.Prj.Tags.Import",
"App.Prj.Tags.Note"
],
"MenuCodes": [
"App.Prj",
"App.Prj.Tags"
],
"DataSourceCodes": []
}
}

View file

@ -1,208 +0,0 @@
{
"Wizard": {
"ComponentKind": 0,
"WizardName": "Milestones",
"ListFormCode": "App.Prj.Milestones",
"MenuCode": "App.Prj.Milestones",
"MenuOrder": 3,
"CreateMenu": true,
"MenuUrl": "/admin/list/App.Prj.Milestones",
"CustomComponentName": "",
"IsTenant": true,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": false,
"ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true,
"Card": false,
"Pivot": false,
"Chart": false,
"Tree": false,
"Gantt": false,
"Scheduler": false,
"Todo": false,
"LanguageTextMenuEn": "Milestones",
"LanguageTextMenuTr": "Dönüm Noktaları",
"LanguageTextTitleEn": "Milestones",
"LanguageTextTitleTr": "Dönüm Noktaları",
"LanguageTextDescEn": "Project milestones",
"LanguageTextDescTr": "Proje dönüm noktaları",
"LanguageTextMenuParentEn": "Project",
"LanguageTextMenuParentTr": "Proje",
"PermissionGroupName": "App.Prj",
"PermissionGroupDisplayNameEn": "Project",
"PermissionGroupDisplayNameTr": "Proje",
"MenuParentCode": "App.Prj",
"MenuParentModuleId": "Prj",
"MenuParentIcon": "FcTimeline",
"MenuIcon": "FcCalendar",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Prj_T_Milestone",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": null,
"StartExpr": null,
"EndExpr": null,
"ProgressExpr": null
},
"GanttOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": "",
"ScaleType": "weeks",
"StartExpr": "",
"EndExpr": "",
"ProgressExpr": "",
"AllowEditing": false,
"AllowTaskAdding": false,
"AllowTaskUpdating": false,
"AllowTaskResourceUpdating": false,
"AllowTaskDeleting": false,
"AllowDependencyAdding": false,
"AllowDependencyDeleting": false,
"AllowResourceAdding": false,
"AllowResourceUpdating": false,
"AllowResourceDeleting": false
},
"SchedulerOptionDto": {
"TextExpr": "",
"UserNameExpr": null,
"DescriptionExpr": null,
"StartDateExpr": "",
"EndDateExpr": "",
"AllDayExpr": "",
"RecurrenceRuleExpr": "",
"RecurrenceExceptionExpr": "",
"StartDayHour": 8,
"EndDayHour": 20,
"DefaultView": "week",
"ShowAllDayPanel": true,
"CellDuration": 30,
"FirstDayOfWeek": 1,
"CrossScrollingEnabled": false,
"AllowResizing": false,
"AllowDragging": false,
"AllowDeleting": false,
"AllowEditing": false,
"AllowAdding": false
},
"TodoOptionDto": {
"TitleExpr": "",
"StatusExpr": "",
"DescriptionExpr": "",
"DueDateExpr": "",
"TagExpr": "",
"AssigneeExpr": "",
"PriorityExpr": "",
"CompletedExpr": "",
"OrderExpr": "",
"StatusOrder": "",
"AllowDragging": true
},
"WorkflowDto": {
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": false,
"Criteria": []
},
"Groups": [
{
"Caption": "Genel",
"ColCount": 2,
"Items": [
{
"FieldName": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"TurkishCaption": "Kimlik",
"EnglishCaption": "Id",
"EditorType": "dxTextBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": false,
"ColSpan": 1
},
{
"FieldName": "Name",
"CaptionName": "App.Listform.ListformField.Name",
"TurkishCaption": "Dönüm Noktası",
"EnglishCaption": "Milestone",
"EditorType": "dxTextBox",
"DbSourceType": 16,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 2,
"EditorOptions": "{\"maxLength\": 300, \"showClearButton\": true}"
},
{
"FieldName": "TargetDate",
"CaptionName": "App.Listform.ListformField.TargetDate",
"TurkishCaption": "Hedef Tarih",
"EnglishCaption": "Target Date",
"EditorType": "dxDateBox",
"DbSourceType": 6,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"type\": \"date\", \"displayFormat\": \"dd.MM.yyyy\", \"useMaskBehavior\": true}"
},
{
"FieldName": "IsCompleted",
"CaptionName": "App.Listform.ListformField.IsCompleted",
"TurkishCaption": "Tamamlandı",
"EnglishCaption": "Completed",
"EditorType": "dxCheckBox",
"DbSourceType": 3,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1
}
]
}
],
"SubForms": [],
"Widgets": []
},
"IsDeletedField": true,
"IsCreatedField": true,
"InsertedRecords": {
"LanguageKeys": [
"App.Prj.Milestones"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Prj.Milestones",
"App.Prj.Milestones.Create",
"App.Prj.Milestones.Update",
"App.Prj.Milestones.Delete",
"App.Prj.Milestones.Export",
"App.Prj.Milestones.Import",
"App.Prj.Milestones.Note"
],
"MenuCodes": [
"App.Prj.Milestones"
],
"DataSourceCodes": []
}
}

View file

@ -1,221 +0,0 @@
{
"Wizard": {
"ComponentKind": 0,
"WizardName": "Customers",
"ListFormCode": "App.Prj.Customers",
"MenuCode": "App.Prj.Customers",
"MenuOrder": 4,
"CreateMenu": true,
"MenuUrl": "/admin/list/App.Prj.Customers",
"CustomComponentName": "",
"IsTenant": true,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": false,
"ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true,
"Card": false,
"Pivot": false,
"Chart": false,
"Tree": false,
"Gantt": false,
"Scheduler": false,
"Todo": false,
"LanguageTextMenuEn": "Customers",
"LanguageTextMenuTr": "Müşteriler",
"LanguageTextTitleEn": "Customers",
"LanguageTextTitleTr": "Müşteriler",
"LanguageTextDescEn": "Customers linked to tasks",
"LanguageTextDescTr": "Görevlerin bağlı olduğu müşteriler",
"LanguageTextMenuParentEn": "Project",
"LanguageTextMenuParentTr": "Proje",
"PermissionGroupName": "App.Prj",
"PermissionGroupDisplayNameEn": "Project",
"PermissionGroupDisplayNameTr": "Proje",
"MenuParentCode": "App.Prj",
"MenuParentModuleId": "Prj",
"MenuParentIcon": "FcTimeline",
"MenuIcon": "FcBusinessContact",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Prj_T_Customer",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": null,
"StartExpr": null,
"EndExpr": null,
"ProgressExpr": null
},
"GanttOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": "",
"ScaleType": "weeks",
"StartExpr": "",
"EndExpr": "",
"ProgressExpr": "",
"AllowEditing": false,
"AllowTaskAdding": false,
"AllowTaskUpdating": false,
"AllowTaskResourceUpdating": false,
"AllowTaskDeleting": false,
"AllowDependencyAdding": false,
"AllowDependencyDeleting": false,
"AllowResourceAdding": false,
"AllowResourceUpdating": false,
"AllowResourceDeleting": false
},
"SchedulerOptionDto": {
"TextExpr": "",
"UserNameExpr": null,
"DescriptionExpr": null,
"StartDateExpr": "",
"EndDateExpr": "",
"AllDayExpr": "",
"RecurrenceRuleExpr": "",
"RecurrenceExceptionExpr": "",
"StartDayHour": 8,
"EndDayHour": 20,
"DefaultView": "week",
"ShowAllDayPanel": true,
"CellDuration": 30,
"FirstDayOfWeek": 1,
"CrossScrollingEnabled": false,
"AllowResizing": false,
"AllowDragging": false,
"AllowDeleting": false,
"AllowEditing": false,
"AllowAdding": false
},
"TodoOptionDto": {
"TitleExpr": "",
"StatusExpr": "",
"DescriptionExpr": "",
"DueDateExpr": "",
"TagExpr": "",
"AssigneeExpr": "",
"PriorityExpr": "",
"CompletedExpr": "",
"OrderExpr": "",
"StatusOrder": "",
"AllowDragging": true
},
"WorkflowDto": {
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": false,
"Criteria": []
},
"Groups": [
{
"Caption": "Genel",
"ColCount": 2,
"Items": [
{
"FieldName": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"TurkishCaption": "Kimlik",
"EnglishCaption": "Id",
"EditorType": "dxTextBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": false,
"ColSpan": 1
},
{
"FieldName": "Name",
"CaptionName": "App.Listform.ListformField.Name",
"TurkishCaption": "Müşteri",
"EnglishCaption": "Customer",
"EditorType": "dxTextBox",
"DbSourceType": 16,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 2,
"EditorOptions": "{\"maxLength\": 300, \"showClearButton\": true}"
},
{
"FieldName": "ContactName",
"CaptionName": "App.Listform.ListformField.ContactName",
"TurkishCaption": "İlgili Kişi",
"EnglishCaption": "Contact",
"EditorType": "dxTextBox",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"maxLength\": 200}"
},
{
"FieldName": "Email",
"CaptionName": "App.Listform.ListformField.Email",
"TurkishCaption": "E-posta",
"EnglishCaption": "Email",
"EditorType": "dxTextBox",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"maxLength\": 200, \"mode\": \"email\"}"
},
{
"FieldName": "Phone",
"CaptionName": "App.Listform.ListformField.Phone",
"TurkishCaption": "Telefon",
"EnglishCaption": "Phone",
"EditorType": "dxTextBox",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"maxLength\": 50}"
}
]
}
],
"SubForms": [],
"Widgets": []
},
"IsDeletedField": true,
"IsCreatedField": true,
"InsertedRecords": {
"LanguageKeys": [
"App.Prj.Customers"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Prj.Customers",
"App.Prj.Customers.Create",
"App.Prj.Customers.Update",
"App.Prj.Customers.Delete",
"App.Prj.Customers.Export",
"App.Prj.Customers.Import",
"App.Prj.Customers.Note"
],
"MenuCodes": [
"App.Prj.Customers"
],
"DataSourceCodes": []
}
}

View file

@ -1,421 +0,0 @@
{
"Wizard": {
"ComponentKind": 0,
"WizardName": "Tasks",
"ListFormCode": "App.Prj.Tasks",
"MenuCode": "App.Prj.Tasks",
"MenuOrder": 1,
"CreateMenu": true,
"MenuUrl": "/admin/list/App.Prj.Tasks",
"CustomComponentName": "",
"IsTenant": true,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": true,
"ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true,
"Card": true,
"Pivot": false,
"Chart": false,
"Tree": false,
"Gantt": true,
"Scheduler": false,
"Todo": true,
"LanguageTextMenuEn": "Tasks",
"LanguageTextMenuTr": "Görevler",
"LanguageTextTitleEn": "Tasks",
"LanguageTextTitleTr": "Görevler",
"LanguageTextDescEn": "Task card and tracking",
"LanguageTextDescTr": "Görev kartı ve takibi",
"LanguageTextMenuParentEn": "Project",
"LanguageTextMenuParentTr": "Proje",
"PermissionGroupName": "App.Prj",
"PermissionGroupDisplayNameEn": "Project",
"PermissionGroupDisplayNameTr": "Proje",
"MenuParentCode": "App.Prj",
"MenuParentModuleId": "Prj",
"MenuParentIcon": "FcTimeline",
"MenuIcon": "FcTodoList",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Prj_T_Task",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeOptionDto": {
"KeyExpr": "",
"ParentIdExpr": "",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": null,
"StartExpr": null,
"EndExpr": null,
"ProgressExpr": null
},
"GanttOptionDto": {
"KeyExpr": "Id",
"ParentIdExpr": "ParentTaskId",
"HasItemsExpr": "",
"RootValue": "",
"ExpandedRowKeys": [],
"AutoExpandAll": false,
"RecursiveSelection": false,
"TitleExpr": "Name",
"ScaleType": "weeks",
"StartExpr": "PlannedStartDate",
"EndExpr": "PlannedEndDate",
"ProgressExpr": "ProgressPercent",
"AllowEditing": false,
"AllowTaskAdding": false,
"AllowTaskUpdating": false,
"AllowTaskResourceUpdating": false,
"AllowTaskDeleting": false,
"AllowDependencyAdding": false,
"AllowDependencyDeleting": false,
"AllowResourceAdding": false,
"AllowResourceUpdating": false,
"AllowResourceDeleting": false
},
"SchedulerOptionDto": {
"TextExpr": "",
"UserNameExpr": null,
"DescriptionExpr": null,
"StartDateExpr": "",
"EndDateExpr": "",
"AllDayExpr": "",
"RecurrenceRuleExpr": "",
"RecurrenceExceptionExpr": "",
"StartDayHour": 8,
"EndDayHour": 20,
"DefaultView": "week",
"ShowAllDayPanel": true,
"CellDuration": 30,
"FirstDayOfWeek": 1,
"CrossScrollingEnabled": false,
"AllowResizing": false,
"AllowDragging": false,
"AllowDeleting": false,
"AllowEditing": false,
"AllowAdding": false
},
"TodoOptionDto": {
"TitleExpr": "Name",
"StatusExpr": "Status",
"DescriptionExpr": "Description",
"DueDateExpr": "PlannedEndDate",
"TagExpr": "TagIds",
"AssigneeExpr": "AssigneeIds",
"PriorityExpr": "",
"CompletedExpr": "",
"OrderExpr": "",
"StatusOrder": "Taslak, Devam Ediyor, Tamamlandı, İptal",
"AllowDragging": true
},
"WorkflowDto": {
"ApprovalUserFieldName": "",
"ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "",
"ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": false,
"Criteria": []
},
"Groups": [
{
"Caption": "Genel",
"ColCount": 2,
"Items": [
{
"FieldName": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"TurkishCaption": "Kimlik",
"EnglishCaption": "Id",
"EditorType": "dxTextBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": false,
"ColSpan": 1
},
{
"FieldName": "Name",
"CaptionName": "App.Listform.ListformField.Name",
"TurkishCaption": "Görev Adı",
"EnglishCaption": "Task Name",
"EditorType": "dxTextBox",
"DbSourceType": 16,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 2,
"EditorOptions": "{\"maxLength\": 300, \"showClearButton\": true}"
},
{
"FieldName": "MilestoneId",
"CaptionName": "App.Listform.ListformField.MilestoneId",
"TurkishCaption": "Dönüm Noktası",
"EnglishCaption": "Milestone",
"EditorType": "dxSelectBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"searchEnabled\": true, \"showClearButton\": true}",
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], Name AS [Name] FROM dbo.Prj_T_Milestone WHERE IsDeleted = 0 ORDER BY Name",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "Status",
"CaptionName": "App.Listform.ListformField.Status",
"TurkishCaption": "Durum",
"EnglishCaption": "Status",
"EditorType": "dxSelectBox",
"DbSourceType": 16,
"IsRequired": true,
"IncludeInEditingForm": true,
"ColSpan": 1,
"LookupDataSourceType": 1,
"LookupQuery": "[{\"key\": \"Taslak\", \"name\": \"Taslak\"}, {\"key\": \"Devam Ediyor\", \"name\": \"Devam Ediyor\"}, {\"key\": \"Tamamlandı\", \"name\": \"Tamamlandı\"}, {\"key\": \"İptal\", \"name\": \"İptal\"}]",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "AssigneeIds",
"CaptionName": "App.Listform.ListformField.AssigneeIds",
"TurkishCaption": "Atananlar",
"EnglishCaption": "Assignees",
"EditorType": "dxTagBox",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"showSelectionControls\": true, \"maxDisplayedTags\": 3, \"searchEnabled\": true, \"multiline\": true}",
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], UserName AS [Name] FROM dbo.AbpUsers WHERE IsDeleted = 0 ORDER BY UserName",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "TagIds",
"CaptionName": "App.Listform.ListformField.TagIds",
"TurkishCaption": "Etiketler",
"EnglishCaption": "Tags",
"EditorType": "dxTagBox",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"showSelectionControls\": true, \"maxDisplayedTags\": 3, \"searchEnabled\": true, \"multiline\": true}",
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], Name AS [Name] FROM dbo.Prj_T_Tag WHERE IsDeleted = 0 ORDER BY Name",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "CustomerId",
"CaptionName": "App.Listform.ListformField.CustomerId",
"TurkishCaption": "Müşteri",
"EnglishCaption": "Customer",
"EditorType": "dxSelectBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"searchEnabled\": true, \"showClearButton\": true}",
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], Name AS [Name] FROM dbo.Prj_T_Customer WHERE IsDeleted = 0 ORDER BY Name",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "CustomerContact",
"CaptionName": "App.Listform.ListformField.CustomerContact",
"TurkishCaption": "İlgili Kişi",
"EnglishCaption": "Contact",
"EditorType": "dxTextBox",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"maxLength\": 200}"
},
{
"FieldName": "PlannedHours",
"CaptionName": "App.Listform.ListformField.PlannedHours",
"TurkishCaption": "İlk Planlanan Saatler",
"EnglishCaption": "Planned Hours",
"EditorType": "dxNumberBox",
"DbSourceType": 7,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"format\": {\"type\": \"fixedPoint\", \"precision\": 2}}"
},
{
"FieldName": "ProgressPercent",
"CaptionName": "App.Listform.ListformField.ProgressPercent",
"TurkishCaption": "İlerleme (%)",
"EnglishCaption": "Progress (%)",
"EditorType": "dxNumberBox",
"DbSourceType": 7,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"format\": {\"type\": \"fixedPoint\", \"precision\": 2}, \"min\": 0, \"max\": 100}"
},
{
"FieldName": "SalesOrderLine",
"CaptionName": "App.Listform.ListformField.SalesOrderLine",
"TurkishCaption": "Satış Sipariş Satırı",
"EnglishCaption": "Sales Order Line",
"EditorType": "dxTextBox",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 2,
"EditorOptions": "{\"maxLength\": 300}"
},
{
"FieldName": "PlannedStartDate",
"CaptionName": "App.Listform.ListformField.PlannedStartDate",
"TurkishCaption": "Planlanan Tarih",
"EnglishCaption": "Planned Date",
"EditorType": "dxDateBox",
"DbSourceType": 6,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"type\": \"datetime\", \"displayFormat\": \"dd.MM.yyyy HH:mm\", \"useMaskBehavior\": true}"
},
{
"FieldName": "PlannedEndDate",
"CaptionName": "App.Listform.ListformField.PlannedEndDate",
"TurkishCaption": "Planlanan Bitiş",
"EnglishCaption": "Planned End",
"EditorType": "dxDateBox",
"DbSourceType": 6,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"type\": \"datetime\", \"displayFormat\": \"dd.MM.yyyy HH:mm\", \"useMaskBehavior\": true}"
},
{
"FieldName": "ParentTaskId",
"CaptionName": "App.Listform.ListformField.ParentTaskId",
"TurkishCaption": "Üst Görev",
"EnglishCaption": "Parent Task",
"EditorType": "dxSelectBox",
"DbSourceType": 9,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"searchEnabled\": true, \"showClearButton\": true}",
"LookupDataSourceType": 2,
"LookupQuery": "SELECT Id AS [Key], Name AS [Name] FROM dbo.Prj_T_Task WHERE IsDeleted = 0 ORDER BY Name",
"ValueExpr": "Key",
"DisplayExpr": "Name"
},
{
"FieldName": "IsFavorite",
"CaptionName": "App.Listform.ListformField.IsFavorite",
"TurkishCaption": "Favori",
"EnglishCaption": "Favorite",
"EditorType": "dxCheckBox",
"DbSourceType": 3,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1
}
]
},
{
"Caption": "Açıklama",
"ColCount": 1,
"Items": [
{
"FieldName": "Description",
"CaptionName": "App.Listform.ListformField.Description",
"TurkishCaption": "Açıklama",
"EnglishCaption": "Description",
"EditorType": "dxHtmlEditor",
"DbSourceType": 16,
"IsRequired": false,
"IncludeInEditingForm": true,
"ColSpan": 1,
"EditorOptions": "{\"valueType\": \"html\", \"height\": 320, \"toolbar\": {\"multiline\": false, \"items\": [\"undo\", \"redo\", \"separator\", \"bold\", \"italic\", \"strike\", \"underline\", \"separator\", \"alignLeft\", \"alignCenter\", \"alignRight\", \"alignJustify\", \"separator\", \"orderedList\", \"bulletList\", \"separator\", \"header\", \"separator\", \"color\", \"background\", \"separator\", \"link\", \"image\", \"separator\", \"clear\", \"codeBlock\", \"blockquote\"]}}"
}
]
}
],
"SubForms": [
{
"TabType": "List",
"TabTitle": "App.Prj.TaskTimesheets",
"Code": "App.Prj.TaskTimesheets",
"IsRefresh": true,
"Relation": [
{
"ParentFieldName": "Id",
"ChildFieldName": "TaskId",
"DbType": 9
}
]
},
{
"TabType": "List",
"TabTitle": "App.Prj.TaskSubTasks",
"Code": "App.Prj.TaskSubTasks",
"IsRefresh": true,
"Relation": [
{
"ParentFieldName": "Id",
"ChildFieldName": "ParentTaskId",
"DbType": 9
}
]
},
{
"TabType": "List",
"TabTitle": "App.Prj.TaskDependencies",
"Code": "App.Prj.TaskDependencies",
"IsRefresh": true,
"Relation": [
{
"ParentFieldName": "Id",
"ChildFieldName": "TaskId",
"DbType": 9
}
]
}
],
"Widgets": []
},
"IsDeletedField": true,
"IsCreatedField": true,
"InsertedRecords": {
"LanguageKeys": [
"App.Prj.Tasks"
],
"PermissionGroupNames": [],
"PermissionNames": [
"App.Prj.Tasks",
"App.Prj.Tasks.Create",
"App.Prj.Tasks.Update",
"App.Prj.Tasks.Delete",
"App.Prj.Tasks.Export",
"App.Prj.Tasks.Import",
"App.Prj.Tasks.Note"
],
"MenuCodes": [
"App.Prj.Tasks"
],
"DataSourceCodes": []
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,7 @@
import { ColumnFormatEditDto } from '@/proxy/admin/list-form-field/models'
import {
GanttOptionDto,
GridOptionsEditDto,
SchedulerOptionDto,
SubFormDto,
TodoOptionDto,
@ -43,6 +45,11 @@ export interface ListFormWizardDto {
editFileName?: string
wizardName: string
/**
* Wizard'ın ilk deploy edildiği an. Dosya adı tarih taşımadığı için seeder dosyaları bu
* damgaya göre sıralar; sunucu üretir, istemci göndermez.
*/
seededAt?: string
listFormCode: string
menuCode: string
menuOrder: number
@ -168,6 +175,10 @@ export interface WizardSeedFileWizardDto extends Omit<ListFormWizardDto, 'groups
export interface WizardSeedFileDto {
wizard: WizardSeedFileWizardDto
/** Wizard'ın ürettiği ekranın ListForm sözleşmesi; tasarımcıdaki değişiklikler buraya yazılır. */
listForm?: GridOptionsEditDto
/** Ekranın alanları; ListForm detayındaki alan tanımlarıyla aynı sözleşme. */
fields?: ColumnFormatEditDto[]
isDeletedField: boolean
isCreatedField: boolean
insertedRecords: WizardInsertedRecordsDto

View file

@ -27,7 +27,6 @@ import {
ColumnFormatEditDto,
CopyListFormFieldColumnDto,
} from '@/proxy/admin/list-form-field/models'
import { LanguageInfo } from '@/proxy/config/models'
import { SelectBoxOption } from '@/types/shared'
import { useStoreState } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -39,7 +38,6 @@ import { FaFileMedical, FaCopy, FaEyeSlash, FaMinus, FaTimes, FaTable } from 're
import { number, object, string } from 'yup'
import FormFieldEdit from './FormFieldEdit'
import { dbSourceTypeOptions, sqlDataTypeToDbType } from '../options'
import { IdentityRoleDto, IdentityUserDto } from '@/proxy/admin/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { EditingFormDto } from '@/proxy/form/models'

View file

@ -17,8 +17,10 @@ import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
import { useEffect, useMemo, useState } from 'react'
import CreatableSelect from 'react-select/creatable'
import {
FaArrowDown,
FaArrowLeft,
FaArrowRight,
FaArrowUp,
FaChevronDown,
FaChevronRight,
FaEdit,
@ -47,6 +49,14 @@ interface TreeNodeProps {
saving: boolean
onStartEdit: (node: MenuTreeNode & { id?: string }) => void
onDelete: (node: MenuTreeNode & { id?: string }) => void
/** Aynı seviyedeki menüler; sıra değişimi bu liste üzerinden hesaplanır. */
siblings: (MenuTreeNode & { id?: string })[]
index: number
onMove: (
siblings: (MenuTreeNode & { id?: string })[],
index: number,
direction: -1 | 1,
) => void
}
function TreeNode({
@ -59,6 +69,9 @@ function TreeNode({
saving,
onStartEdit,
onDelete,
siblings,
index,
onMove,
}: TreeNodeProps) {
const hasChildren = node.children.length > 0
const isExpanded = expanded.has(node.code)
@ -114,6 +127,34 @@ function TreeNode({
</span>
<span className="shrink-0 flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
type="button"
disabled={saving || index === 0}
title={translate('::App.Platform.MoveUp')}
onClick={(e) => {
e.stopPropagation()
onMove(siblings, index, -1)
}}
size="xs"
variant="plain"
shape="circle"
icon={<FaArrowUp className="text-xs" />}
className={`!h-6 !w-6 !px-0 hover:!bg-transparent ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-indigo-500'}`}
/>
<Button
type="button"
disabled={saving || index === siblings.length - 1}
title={translate('::App.Platform.MoveDown')}
onClick={(e) => {
e.stopPropagation()
onMove(siblings, index, 1)
}}
size="xs"
variant="plain"
shape="circle"
icon={<FaArrowDown className="text-xs" />}
className={`!h-6 !w-6 !px-0 hover:!bg-transparent ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-indigo-500'}`}
/>
<Button
type="button"
disabled={saving}
@ -146,7 +187,7 @@ function TreeNode({
</div>
{isExpanded &&
node.children.map((child) => (
node.children.map((child, childIndex) => (
<TreeNode
key={child.code}
node={child as MenuTreeNode & { id?: string }}
@ -158,6 +199,9 @@ function TreeNode({
saving={saving}
onStartEdit={onStartEdit}
onDelete={onDelete}
siblings={node.children as (MenuTreeNode & { id?: string })[]}
index={childIndex}
onMove={onMove}
/>
))}
</div>
@ -237,6 +281,50 @@ function MenuTreeInline({
}
}
/**
* Kardeşler arasında sıra değiştirir. Yeni sıra numaraları kardeşlerin mevcut değerlerinden
* alınır; böylece görünmeyen (link) menülerin araya giren sıraları bozulmaz.
*/
const handleMove = async (
siblings: (MenuTreeNode & { id?: string })[],
index: number,
direction: -1 | 1,
) => {
const targetIndex = index + direction
if (targetIndex < 0 || targetIndex >= siblings.length) return
const itemByCode = new Map(rawItems.map((item) => [item.code, item]))
const current = siblings.map((s) => itemByCode.get(s.code))
if (current.some((item) => !item?.id)) return
const reordered = current as MenuItem[]
const moved = [...reordered]
const [item] = moved.splice(index, 1)
moved.splice(targetIndex, 0, item)
const slots = reordered.map((i) => i.order ?? 0).sort((a, b) => a - b)
let previous = Number.NEGATIVE_INFINITY
const payload = moved.reduce<MenuItem[]>((acc, entry, i) => {
const order = Math.max(slots[i], previous + 1)
previous = order
if (entry.order !== order) acc.push({ ...entry, order })
return acc
}, [])
if (payload.length === 0) return
setSaving(true)
try {
await menuService.updateAll(payload)
onReload()
} catch (e: any) {
toast.push(<Notification title={e.message} type="danger" />, {
placement: 'bottom-end',
})
} finally {
setSaving(false)
}
}
// Menü ağacı her render'da yeniden dolaşılmasın; id eşlemesi tek haritadan yapılır.
const enrichedNodes = useMemo(() => {
const idByCode = new Map(rawItems.map((item) => [item.code, item.id]))
@ -254,6 +342,7 @@ function MenuTreeInline({
saving,
onStartEdit: onEditMenu,
onDelete: handleDelete,
onMove: handleMove,
}
return (
@ -269,13 +358,15 @@ function MenuTreeInline({
{translate('::App.WizardStep1.NoMenusAvailable')}
</div>
) : (
enrichedNodes.map((node) => (
enrichedNodes.map((node, index) => (
<TreeNode
key={node.code}
node={node}
depth={0}
selectedCode={value}
onSelect={onChange}
siblings={enrichedNodes}
index={index}
{...sharedNodeProps}
/>
))

View file

@ -24,11 +24,14 @@ import {
FaTasks,
FaChartBar,
FaRegCalendarAlt,
FaToggleOn,
FaToggleOff,
} from 'react-icons/fa'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { getMenus } from '@/services/menu.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { CascadeBehavior, SqlTableRelation, RelationshipType } from '@/proxy/developerKit/models'
import type { CrudEndpoint } from '@/proxy/developerKit/models'
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
import { MenuItem } from '@/proxy/menus/menu'
import {
@ -45,6 +48,7 @@ import {
CRUD_ENDPOINT_PERMISSION,
CRUD_OPERATION_TYPES,
type CrudOperationType,
METHOD_COLOR,
getErrorMessage,
getOperationMeta,
toEntityName,
@ -1333,6 +1337,9 @@ const SqlTableDesignerDialog = ({
const [crudOperations, setCrudOperations] = useState<CrudOperationType[]>([
...CRUD_OPERATION_TYPES,
])
/** Duzenlenen tablonun kayitli endpoint'leri; adim mevcut durumu gosterir. */
const [existingCrudEndpoints, setExistingCrudEndpoints] = useState<CrudEndpoint[]>([])
const [crudLoading, setCrudLoading] = useState(false)
const [columns, setColumns] = useState<ColumnDefinition[]>([createEmptyColumn()])
const [originalColumns, setOriginalColumns] = useState<ColumnDefinition[]>([])
const [colsLoading, setColsLoading] = useState(false)
@ -1418,6 +1425,24 @@ const SqlTableDesignerDialog = ({
.catch(() => {})
}
// Edit mode: kayitli CRUD endpoint'leri okunur; varsa adim mevcut durumla acilir.
if (initialTableData && canManageCrudEndpoints) {
setCrudLoading(true)
developerKitService
.getEndpointsByEntity(toEntityName(initialTableData.tableName))
.then((endpoints) => {
setExistingCrudEndpoints(endpoints)
if (endpoints.length === 0) return
setGenerateCrud(true)
const active = endpoints
.filter((e) => e.isActive)
.map((e) => e.operationType as CrudOperationType)
setCrudOperations(CRUD_OPERATION_TYPES.filter((op) => active.includes(op)))
})
.catch(() => setExistingCrudEndpoints([]))
.finally(() => setCrudLoading(false))
}
// Edit mode: load table's existing columns + FK constraints
if (initialTableData && dataSource) {
setColsLoading(true)
@ -1541,7 +1566,7 @@ const SqlTableDesignerDialog = ({
.catch(() => {})
.finally(() => setIndexesLoading(false))
}
}, [isOpen, dataSource, initialTableData])
}, [isOpen, dataSource, initialTableData, canManageCrudEndpoints])
const generatedSql = useMemo(
() =>
@ -2126,6 +2151,7 @@ const SqlTableDesignerDialog = ({
setTemplatesOpen(false)
setGenerateCrud(false)
setCrudOperations([...CRUD_OPERATION_TYPES])
setExistingCrudEndpoints([])
onClose()
}
@ -3343,6 +3369,43 @@ const SqlTableDesignerDialog = ({
const renderCrudEndpoints = () => (
<div className="flex flex-col gap-4">
{crudLoading && (
<p className="mb-0 text-xs text-gray-400">{translate('::App.Platform.Loading')}</p>
)}
{existingCrudEndpoints.length > 0 && (
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-900 dark:bg-blue-900/20">
<span className="block text-sm font-medium">
{translate('::App.SqlQueryManager.ExistingCrudEndpoints')}
</span>
<span className="mb-2 block text-xs text-gray-500 dark:text-gray-400">
{translate('::App.SqlQueryManager.ExistingCrudEndpointsHint')}
</span>
<div className="flex flex-col gap-1">
{existingCrudEndpoints.map((endpoint) => (
<div key={endpoint.id} className="flex items-center gap-2 text-xs">
<span
className={`rounded border px-1.5 py-0.5 font-mono ${
METHOD_COLOR[endpoint.method] ||
'border-gray-200 bg-gray-100 text-gray-700 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-200'
}`}
>
{endpoint.method}
</span>
<span className="font-mono text-gray-600 dark:text-gray-300">{endpoint.path}</span>
<span className="ml-auto">
{endpoint.isActive ? (
<FaToggleOn className="text-base text-green-500" />
) : (
<FaToggleOff className="text-base text-gray-300 dark:text-gray-600" />
)}
</span>
</div>
))}
</div>
</div>
)}
<label className="flex items-start gap-3 cursor-pointer rounded-lg border p-3 dark:border-gray-700">
<Checkbox checked={generateCrud} onChange={(checked) => setGenerateCrud(checked)} />
<span>

View file

@ -53,6 +53,13 @@ type GanttTreeListInstance = {
}
}
// Gorev listesi kolon sayisina gore acilir; DevExtreme TreeList sabit width verilmezse
// kolonlari mevcut genislige esit paylastirip kolon arttikca hepsini okunamaz hale getiriyor.
const DEFAULT_TASK_LIST_COLUMN_WIDTH = 100
const MIN_TASK_LIST_WIDTH = 300
const MAX_TASK_LIST_WIDTH_RATIO = 0.8
const TASK_LIST_WIDTH_PADDING = 2
const getGanttTreeList = (gantt: GanttInstance) => {
const element = (gantt.element() as HTMLElement).querySelector('.dx-treelist')
return element
@ -89,7 +96,7 @@ const GanttView = (props: GanttViewProps) => {
const [columnData, setColumnData] = useState<GridColumnData[]>()
const gridDto = useListFormGridDto(listFormCode, extGridDto)
const [scaleType, setScaleType] = useState<GanttScaleType>('weeks')
const [taskListWidth, setTaskListWidth] = useState(500)
const [taskListWidth, setTaskListWidth] = useState(MIN_TASK_LIST_WIDTH)
const layout = layoutTypes.gantt || 'gantt'
const pendingStateRef = useRef<Record<string, any> | undefined>(undefined)
const loadedStateKeyRef = useRef('')
@ -171,11 +178,47 @@ const GanttView = (props: GanttViewProps) => {
return (columnData ?? [])
.filter((column: any) => column.type !== 'buttons')
.map((column: any) =>
chartFields.has(column.dataField) ? { ...column, visible: false } : column,
)
.map((column: any) => ({
...column,
// width baslangic degeridir; alt sinir konmadigi icin fareyle serbestce daraltilabilir.
width: column.width ?? DEFAULT_TASK_LIST_COLUMN_WIDTH,
...(chartFields.has(column.dataField) ? { visible: false } : {}),
}))
}, [columnData, gridDto])
// Kullanici bolucuyu surukledigi ya da kayitli gorunum yuklendigi anda otomatik
// genislik devreden cikar; sonrasinda panel yalnizca fareyle degisir.
const taskListWidthLockedRef = useRef(false)
const clampTaskListWidth = useCallback((width: number) => {
const ganttWidth =
(gridRef.current?.instance()?.element() as HTMLElement | undefined)?.getBoundingClientRect()
.width || window.innerWidth
const upperBound = Math.max(
MIN_TASK_LIST_WIDTH,
Math.round(ganttWidth * MAX_TASK_LIST_WIDTH_RATIO),
)
return Math.min(Math.max(Math.round(width), MIN_TASK_LIST_WIDTH), upperBound)
}, [])
const defaultTaskListWidth = useMemo(() => {
const visibleColumns = ganttColumns.filter((column: any) => column.visible !== false)
if (visibleColumns.length === 0) return MIN_TASK_LIST_WIDTH
return clampTaskListWidth(
visibleColumns.reduce(
(sum: number, column: any) => sum + (column.width ?? DEFAULT_TASK_LIST_COLUMN_WIDTH),
TASK_LIST_WIDTH_PADDING,
),
)
}, [clampTaskListWidth, ganttColumns])
useEffect(() => {
if (taskListWidthLockedRef.current) return
setTaskListWidth(defaultTaskListWidth)
}, [defaultTaskListWidth])
useEffect(() => setColumnData(memoizedColumns), [memoizedColumns])
useEffect(() => setGanttDataSource(memoizedDataSource), [memoizedDataSource])
@ -274,7 +317,10 @@ const GanttView = (props: GanttViewProps) => {
treeList.state(treeListState ?? treeState)
if (savedScaleType) setScaleType(savedScaleType)
if (savedTaskListWidth) setTaskListWidth(savedTaskListWidth)
if (savedTaskListWidth) {
taskListWidthLockedRef.current = true
setTaskListWidth(savedTaskListWidth)
}
gantt.option({
...(showDependencies !== undefined ? { showDependencies } : {}),
...(showResources !== undefined ? { showResources } : {}),
@ -282,8 +328,39 @@ const GanttView = (props: GanttViewProps) => {
})
}, [])
const splitterBoundRef = useRef(false)
const columnResizingModeRef = useRef(false)
const handleGanttContentReady = useCallback(
(event: ContentReadyEvent) => {
// Gantt ic TreeList'i 'nextColumn' ile olusturuyor; kolon daraltinca fark
// komsu kolona gidiyordu.
if (!columnResizingModeRef.current) {
const treeList = getGanttTreeList(event.component)
if (treeList) {
columnResizingModeRef.current = true
treeList.option('columnResizingMode', 'widget')
}
}
// DevExtreme, bolucu suruklemesini taskListWidth option'ina yazmadigi icin
// kullanicinin elle ayarladigi genislik olaydan yakalanip kilitlenir.
if (!splitterBoundRef.current) {
const splitter = (event.component.element() as HTMLElement).querySelector(
'.dx-splitter-wrapper',
)
if (splitter) {
splitterBoundRef.current = true
splitter.addEventListener(
'pointerdown',
() => {
taskListWidthLockedRef.current = true
},
{ capture: true },
)
}
}
if (!pendingStateRef.current) return
const state = pendingStateRef.current
@ -309,8 +386,9 @@ const GanttView = (props: GanttViewProps) => {
customizationData: '',
})
getGanttTreeList(gantt)?.state(null)
setTaskListWidth(500)
}, [listFormCode, storageKey])
taskListWidthLockedRef.current = false
setTaskListWidth(defaultTaskListWidth)
}, [defaultTaskListWidth, listFormCode, storageKey])
useEffect(() => {
if (