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 → - **Katman sınırı**: Domain.Shared → Domain → Application.Contracts → Application →
HttpApi → HttpApi.Host. Ters yönde referans verilmez; `EntityFrameworkCore` yalnızca 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 - **AppService**: `PlatformAppService`'ten türer, arayüzü Application.Contracts'ta
tanımlanır. Controller yazılmaz — ABP otomatik API üretir. Controller sadece ABP tanımlanır. Controller yazılmaz — ABP otomatik API üretir. Controller sadece ABP
konvansiyonunun karşılamadığı durumda (dosya indirme, harici webhook) eklenir. konvansiyonunun karşılamadığı durumda (dosya indirme, harici webhook) eklenir.

View file

@ -16,8 +16,11 @@ sorusunda buraya bak.
**Ortak kurallar** **Ortak kurallar**
- Bütün `*Json` kolonları veritabanında **tek satır JSON string** olarak durur; wizard seed - Bütün `*Json` kolonları veritabanında **tek satır JSON string** olarak durur. Wizard seed
dosyasına yazarken kaçışlanır. 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. - 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). - `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 - **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) ├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise)
├── crud/{EntityName}.json ← tablonun REST uçları ├── crud/{EntityName}.json ← tablonun REST uçları
├── custom/{ComponentName}.json ← Custom Component (Visual Designer dokümanı ya da kod) ├── 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. 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ı | | 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 | | 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 | | 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 | | 4 | Wizard | `wizard/{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 | | 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 **`Wizard.SeededAt` bir bağımlılık aracıdır.** Dosya adı tarih taşımaz (`{WizardName}.json`);
göre sıralı** işler. Bir wizard başka bir wizard'ın ürettiği menüye/ListForm'a dayanıyorsa, `WizardDataSeeder` `wizard/` klasörünü **`Wizard.SeededAt` damgasına göre sıralı** işler (eşitlikte
bağımlı olanın zaman damgası **daha büyük** olmalıdır. Alt form olarak kullanılacak menüsüz dosya adı). Bir wizard başka bir wizard'ın ürettiği menüye/ListForm'a dayanıyorsa, bağımlı olanın
ekranı her zaman önce numaralandır. 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 ### 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 | | 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`) | | 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) | | 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 | | İç içe JSON | `EditorOptions`, `EditorScript`, `LookupQuery` → JSON **string** olarak kaçışlanır |
| Satır sonu | LF | | 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` | | 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 (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` | | 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` | | Custom component seed | `{Name}.json` | `OrderBoard.json` |
| CRUD endpoint seed | `{EntityName}.json` | `Mrp_T_Order.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ı) ## 4. Artefakt A — ListForm ekranı (Wizard seed dosyası)
Konum: `configs/seeds/{kapsam}/wizard/{yyyyMMddHHmmss}_{WizardName}.json` Konum: `configs/seeds/{kapsam}/wizard/{WizardName}.json` (dosya adında tarih yoktur)
Okuyan: `WizardDataSeeder` · Yazan: `ListFormWizardAppService` Okuyan: `WizardDataSeeder` · Yazan: `ListFormWizardAppService` ve `WizardSeedSynchronizer`
### 4.1 Dosya iskeleti ### 4.1 Dosya iskeleti
```jsonc ```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) "IsDeletedField": true, // tabloda IsDeleted var mı (soft delete)
"IsCreatedField": true, // tabloda CreatorId var mı (audit) "IsCreatedField": true, // tabloda CreatorId var mı (audit)
"InsertedRecords": { // bu çalıştırmanın GERÇEKTEN yarattığı kayıtlar "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 `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. 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. Elle dosya yazarken buraya var olan bir kaydı koyma — silme onu da götürür.
### 4.2 `Wizard` bloğu — alan alan ### 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** **Kimlik ve yol**
| Alan | Anlam | | Alan | Anlam |
| --- | --- | | --- | --- |
| `ComponentKind` | `0` List (varsayılan) · `1` Custom. Dosyadaki ilk alan olmalı. | | `ComponentKind` | `0` List (varsayılan) · `1` Custom. Dosyadaki ilk alan olmalı. |
| `WizardName` | Dosya adının ve export zip adının kaynağı. | | `WizardName` | Dosya adının (`{WizardName}.json`) ve export zip adının kaynağı. |
| `ListFormCode` | Ekranın kodu; route ve metadata bu koda göre çözülür. | | `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. | | `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. | | `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ı. | | `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 | | 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. | | `MenuParentCode` / `MenuParentModuleId` / `MenuParentIcon` | Üst menü; yoksa oluşturulur. |
| `MenuIcon` | React-icons adı (`FcBiohazard`, `FaBox`…). | | `MenuIcon` | React-icons adı (`FcBiohazard`, `FaBox`…). |
| `MenuOrder` | Kardeşler arası sıra. | | `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 | | 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ı. | | `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. | | `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. | | `LanguageTextMenuEn` / `Tr` | Menü etiketi. |
@ -624,23 +675,23 @@ edilir.
| Alan | Anlam | | Alan | Anlam |
| --- | --- | | --- | --- |
| `DataSourceCode` | Bağlantı kodu; varsayılan `"Default"`. | | `DataSourceCode` | `[ListForm]` Bağlantı kodu; varsayılan `"Default"`. |
| `DataSourceConnectionString` | Yeni bir bağlantı tanımlanıyorsa; aksi hâlde boş. **Asla gerçek kimlik bilgisi yazma.** | | `DataSourceConnectionString` | Yalnızca **yeni** bir bağlantı tanımlanıyorsa doldurulur; boşsa dosyaya hiç yazılmaz. **Asla gerçek kimlik bilgisi yazma.** |
| `SelectCommandType` | Bkz. §3. | | `SelectCommandType` | `[ListForm]` Bkz. §3. |
| `SelectCommand` | Tablo/view adı, fonksiyon çağrısı ya da sorgu metni. | | `SelectCommand` | `[ListForm]` Tablo/view adı, fonksiyon çağrısı ya da sorgu metni. |
| `KeyFieldName` / `KeyFieldDbSourceType` | Birincil anahtar ve tipi. | | `KeyFieldName` / `KeyFieldDbSourceType` | `[ListForm]` Birincil anahtar ve tipi. |
**Davranış** **Davranış**
| Alan | Anlam | | Alan | Anlam |
| --- | --- | | --- | --- |
| `IsTenant` / `IsBranch` / `IsOrganizationUnit` | Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. | | `IsTenant` / `IsBranch` / `IsOrganizationUnit` | `[ListForm]` Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. |
| `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `ConfirmDelete` | Toolbar ve satır aksiyonları. | | `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `ConfirmDelete` | `[ListForm]` Toolbar ve satır aksiyonları (`EditingOptionDto`). |
| `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. | | `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` | `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. | | `DefaultLayout` | `[ListForm]` `"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. | | `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 | | 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: Her `Items` öğesi:
| Alan | Anlam | | Alan | Anlam |
@ -673,16 +729,16 @@ Her `Items` öğesi:
| `FieldName` | Veri kolonunun adı (SQL'deki adıyla birebir). | | `FieldName` | Veri kolonunun adı (SQL'deki adıyla birebir). |
| `CaptionName` | Dil anahtarı ya da düz başlık. | | `CaptionName` | Dil anahtarı ya da düz başlık. |
| `TurkishCaption` / `EnglishCaption` | Dil metinleri; ikisi de doldurulur. | | `TurkishCaption` / `EnglishCaption` | Dil metinleri; ikisi de doldurulur. |
| `EditorType` | Bkz. §3. | | `EditorType` | `[Fields]` Bkz. §3. |
| `EditorOptions` | DevExtreme editör JSON'u — §6. | | `EditorOptions` | `[Fields]` DevExtreme editör JSON'u — §6. |
| `EditorScript` | Alan davranış script'i — §5. | | `EditorScript` | `[Fields]` Alan davranış script'i — §5. |
| `DbSourceType` | Bkz. §3. | | `DbSourceType` | `[Fields]` Bkz. §3. |
| `IsRequired` | Zorunluluk. | | `IsRequired` | `[Fields]` Zorunluluk (`ValidationRuleDto`). |
| `IncludeInEditingForm` | `false` ise yalnızca grid sütunu olur, forma girmez. | | `IncludeInEditingForm` | `[Fields]` `false` ise yalnızca grid sütunu olur, forma girmez (`EditGroupOrderNo`). |
| `ColSpan` | Formda kaç kolon kaplar (grubun `ColCount` değeri içinde). | | `ColSpan` | `[Fields]` Formda kaç kolon kaplar (grubun `ColCount` değeri içinde). |
| `LookupDataSourceType` | `1/2/3` — bkz. §3. `0` = lookup yok. | | `LookupDataSourceType` | `[Fields]` `1/2/3` — bkz. §3. `0` = lookup yok. |
| `LookupQuery` | `Query` tipinde SQL; `StaticData` tipinde JSON dizisi; `WebService` tipinde URL. | | `LookupQuery` | `[Fields]` `Query` tipinde SQL; `StaticData` tipinde JSON dizisi; `WebService` tipinde URL. |
| `ValueExpr` / `DisplayExpr` | Lookup'ın değer ve etiket kolonları. | | `ValueExpr` / `DisplayExpr` | `[Fields]` Lookup'ın değer ve etiket kolonları. |
### 4.3.1 Sütunlar nasıl otomatik üretilir ### 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": { "Wizard": {
"ComponentKind": 0, "ComponentKind": 0,
"WizardName": "Orders", "WizardName": "Orders",
"SeededAt": "2026-08-26T10:15:00",
"ListFormCode": "App.Mrp.Orders", "ListFormCode": "App.Mrp.Orders",
"MenuCode": "App.Mrp.Orders", "MenuCode": "App.Mrp.Orders",
"MenuOrder": 1, "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. Ayrıntı: `lowcode-reference.instructions.md` §5.3.
Alt form olarak kullanılacak ekranı **menüsüz wizard** (`CreateMenu: false`) ile üret; menüde 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). wizard'ınkinden **küçük** olmalıdır (§0.2).
### 4.7 İş akışı (onay süreci) ### 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/custom/RoleComponent.json
configs/seeds/host/crud/AbpRoles.json · AbpUsers.json · AbpUserRoles.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: Nasıl bağlanıyorlar:
@ -3419,8 +3476,8 @@ configs/seeds/host/
├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…) ├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…)
├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir ├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir
├── custom/AdvanceRequestForm.json ← yalnızca serbest yerleşim isteniyorsa ├── custom/AdvanceRequestForm.json ← yalnızca serbest yerleşim isteniyorsa
├── wizard/20260826101000_AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, önce ├── wizard/AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, küçük SeededAt
└── wizard/20260826101500_AdvanceRequests.json ← ana ekran + menü + yetki + dil + onay akışı └── wizard/AdvanceRequests.json ← ana ekran + menü + yetki + dil + onay akışı
``` ```
**Karar:** Talep formu **standart bir kayıt listesi + düzenleme formu + onay akışı** ise **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: Prompt ile üretim yaptıysan ek olarak:
- [ ] Dosyalar doğru kapsam klasöründe (`host/` ya da `tenants/{tenantId}/`). - [ ] 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 - [ ] Tablo/kolon adları uydurulmadı; ya var olan nesneden okundu ya da `sql/object/` altında
üretildi. üretildi.
- [ ] `KeyFieldDbSourceType`, tablodaki `Id` tipiyle uyumlu. - [ ] `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`) 5. **Permission** kayıtları (`.Default`, `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note`)
6. **Dil anahtarları** (menü, başlık, açıklama; EN + TR) 6. **Dil anahtarları** (menü, başlık, açıklama; EN + TR)
7. Gerekiyorsa **ayar/entegrasyon** bağımlılıkları 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ı 9. **Doğrulama ve geri alma** notları
**Adım bileşenleri.** Adımlar numaralı dosya adları yerine işlevleriyle adlandırılır ve **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 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. 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 **İ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ı dosyasında ilk alandır. Varsayılan `List` olduğu için bu alanı taşımayan eski seed dosyaları
eskisi gibi çalışır. eskisi gibi çalışır.
@ -1165,7 +1175,7 @@ geri yüklenir. Kapsam klasörü CDN düzeniyle aynıdır ve `SeedPathResolver`
``` ```
configs/seeds/ configs/seeds/
├── host/ # host kapsamı ├── host/ # host kapsamı
│ ├── wizard/ # {zaman damgası}_{Ad}.json → WizardDataSeeder │ ├── wizard/ # {Ad}.json → WizardDataSeeder
│ ├── custom/ # {ComponentName}.json → CustomComponentDataSeeder │ ├── custom/ # {ComponentName}.json → CustomComponentDataSeeder
│ ├── crud/ # {EntityName}.json → CrudDataSeeder │ ├── crud/ # {EntityName}.json → CrudDataSeeder
│ ├── sql/{object,execute}/ # .sql (SQL Server) │ ├── 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. > 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ı > 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); } 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); } 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.Data;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Sozsoft.Languages.Languages;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
namespace Sozsoft.Platform.ListForms; 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 public class ListFormWizardDto
{ {
/// <summary> /// <summary>
@ -23,6 +38,13 @@ public class ListFormWizardDto
public string EditFileName { get; set; } public string EditFileName { get; set; }
public string WizardName { 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 ListFormCode { get; set; }
public string MenuCode { get; set; } public string MenuCode { get; set; }
public int MenuOrder { get; set; } public int MenuOrder { get; set; }
@ -107,4 +129,450 @@ public class ListFormWizardDto
public List<WizardColumnGroupInputDto> Groups { get; set; } = new(); public List<WizardColumnGroupInputDto> Groups { get; set; } = new();
public List<SubFormDto> SubForms { get; set; } = new(); public List<SubFormDto> SubForms { get; set; } = new();
public List<WidgetEditDto> Widgets { 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 Sozsoft.Platform.Enums;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.ListForms; namespace Sozsoft.Platform.ListForms;
@ -12,6 +17,15 @@ public class WizardSeedFileDto
{ {
public ListFormWizardDto Wizard { get; set; } 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> /// <summary>Tabloda IsDeleted alanı olup olmadığını belirtir (soft delete desteği).</summary>
public bool IsDeletedField { get; set; } 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. /// Daha önce var olan kayıtlar buraya eklenmez, silme işleminde sadece bu liste kullanılır.
/// </summary> /// </summary>
public WizardInsertedRecordsDto InsertedRecords { get; set; } = new(); 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> /// <summary>

View file

@ -21,12 +21,16 @@ public class ListFormFieldsAppService : CrudAppService<
{ {
public IRepository<ListForm, Guid> RepoListForm { get; } public IRepository<ListForm, Guid> RepoListForm { get; }
private readonly IWizardSeedSynchronizer wizardSeedSynchronizer;
public ListFormFieldsAppService( public ListFormFieldsAppService(
IRepository<ListFormField, Guid> _repository, IRepository<ListFormField, Guid> _repository,
IRepository<ListForm, Guid> _repoListForm IRepository<ListForm, Guid> _repoListForm,
IWizardSeedSynchronizer _wizardSeedSynchronizer
) : base(_repository) ) : base(_repository)
{ {
RepoListForm = _repoListForm; RepoListForm = _repoListForm;
wizardSeedSynchronizer = _wizardSeedSynchronizer;
CreatePolicyName = $"{AppCodes.Listforms.ListformField}.Create"; CreatePolicyName = $"{AppCodes.Listforms.ListformField}.Create";
UpdatePolicyName = $"{AppCodes.Listforms.ListformField}.Update"; 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) 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 Repository.UpdateAsync(item, autoSave: true);
await wizardSeedSynchronizer.SyncAsync(item.ListFormCode);
return await MapToGetOutputDtoAsync(item); 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")] [Authorize(AppCodes.Listforms.ListformField + ".Create")]
public async Task<CopyListFormFieldColumnDto> CopyField(CopyListFormFieldColumnDto input) public async Task<CopyListFormFieldColumnDto> CopyField(CopyListFormFieldColumnDto input)
{ {
@ -324,6 +344,8 @@ public class ListFormFieldsAppService : CrudAppService<
await Repository.InsertAsync(field); await Repository.InsertAsync(field);
await wizardSeedSynchronizer.SyncAsync(field.ListFormCode);
return input; return input;
} }
} }

View file

@ -20,12 +20,15 @@ namespace Sozsoft.Platform.ListForms.Administration;
public class ListFormJsonRowAppService : PlatformAppService public class ListFormJsonRowAppService : PlatformAppService
{ {
private readonly IRepository<ListForm, Guid> repository; private readonly IRepository<ListForm, Guid> repository;
private readonly IWizardSeedSynchronizer wizardSeedSynchronizer;
public ListFormJsonRowAppService( public ListFormJsonRowAppService(
IRepository<ListForm, Guid> _repository IRepository<ListForm, Guid> _repository,
IWizardSeedSynchronizer _wizardSeedSynchronizer
) : base() ) : base()
{ {
repository = _repository; repository = _repository;
wizardSeedSynchronizer = _wizardSeedSynchronizer;
} }
public async Task<dynamic> GetAllAsync(Guid id, string field) public async Task<dynamic> GetAllAsync(Guid id, string field)
@ -183,6 +186,9 @@ public class ListFormJsonRowAppService : PlatformAppService
} }
await repository.UpdateAsync(listForm); 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) public async Task UpdateAsync(Guid id, CrudFieldsDefaultValueJsonItemDto model)
@ -237,6 +243,9 @@ public class ListFormJsonRowAppService : PlatformAppService
} }
await repository.UpdateAsync(listForm); 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) public async Task DeleteAsync(Guid id, string field, int index)
@ -291,6 +300,9 @@ public class ListFormJsonRowAppService : PlatformAppService
} }
await repository.UpdateAsync(listForm); 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) private async Task<ListForm> GetAsync(Guid id)

View file

@ -21,12 +21,15 @@ public class ListFormsAppService : CrudAppService<
GridOptionsEditDto>, IListFormsAppService GridOptionsEditDto>, IListFormsAppService
{ {
private readonly IListFormAuthorizationManager authManager; private readonly IListFormAuthorizationManager authManager;
private readonly IWizardSeedSynchronizer wizardSeedSynchronizer;
public ListFormsAppService( public ListFormsAppService(
IRepository<ListForm, Guid> _repository, IRepository<ListForm, Guid> _repository,
IListFormAuthorizationManager _authManager) : base(_repository) IListFormAuthorizationManager _authManager,
IWizardSeedSynchronizer _wizardSeedSynchronizer) : base(_repository)
{ {
authManager = _authManager; authManager = _authManager;
wizardSeedSynchronizer = _wizardSeedSynchronizer;
CreatePolicyName = $"{AppCodes.Listforms.Listform}.Create"; CreatePolicyName = $"{AppCodes.Listforms.Listform}.Create";
UpdatePolicyName = $"{AppCodes.Listforms.Listform}.Update"; UpdatePolicyName = $"{AppCodes.Listforms.Listform}.Update";
@ -239,6 +242,9 @@ public class ListFormsAppService : CrudAppService<
await Repository.UpdateAsync(item, autoSave: true); 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); return await MapToGetOutputDtoAsync(item);
} }
} }

View file

@ -74,7 +74,6 @@ public class ListFormWizardAppService(
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage; private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
private static readonly JsonSerializerOptions SeedJsonReadOptions = new() { PropertyNameCaseInsensitive = true }; private static readonly JsonSerializerOptions SeedJsonReadOptions = new() { PropertyNameCaseInsensitive = true };
private static readonly JsonSerializerOptions SeedJsonWriteOptions = new() { WriteIndented = true };
[UnitOfWork] [UnitOfWork]
public async Task Create(ListFormWizardDto input) public async Task Create(ListFormWizardDto input)
@ -92,18 +91,18 @@ public class ListFormWizardAppService(
: AppCodes.Listforms.Wizards.Create); : AppCodes.Listforms.Wizards.Create);
if (isEdit) if (isEdit)
{
// Ilk deploy zamani wizard'in kimligidir; duzenleme onu degistirmez.
input.SeededAt = await ReadSeededAtAsync(editFileName!);
await DeleteFileInternalAsync(editFileName!); await DeleteFileInternalAsync(editFileName!);
}
var wizardName = input.WizardName?.Trim(); var wizardName = input.WizardName?.Trim();
if (string.IsNullOrWhiteSpace(wizardName)) if (string.IsNullOrWhiteSpace(wizardName))
throw new UserFriendlyException("Wizard adı zorunludur."); throw new UserFriendlyException("Wizard adı zorunludur.");
var code = string.IsNullOrWhiteSpace(input.MenuCode) var code = input.ResolveMenuCode();
? WizardConsts.WizardKey(wizardName) var listFormCode = input.ResolveListFormCode();
: input.MenuCode.Trim();
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
? code
: input.ListFormCode.Trim();
// Aşağıdaki tüm kayıtlar normalize edilmiş kodlara göre üretilir. // Aşağıdaki tüm kayıtlar normalize edilmiş kodlara göre üretilir.
input.ListFormCode = listFormCode; input.ListFormCode = listFormCode;
input.MenuCode = code; input.MenuCode = code;
@ -295,7 +294,7 @@ public class ListFormWizardAppService(
if (isCustomComponent) if (isCustomComponent)
{ {
await _languageTextAppService.ClearRedisCacheAsync(); await _languageTextAppService.ClearRedisCacheAsync();
await SaveWizardSeedFileAsync(input, false, false, inserted); await SaveWizardSeedFileAsync(input, null, [], false, false, inserted);
return; return;
} }
@ -318,21 +317,7 @@ public class ListFormWizardAppService(
inserted.DataSourceCodes.Add(input.DataSourceCode); inserted.DataSourceCodes.Add(input.DataSourceCode);
} }
// Build EditingFormJson from wizard groups (sadece editing form alanı olan gruplar) //ListForm ve alanlari - varsa sil, yeniden uret
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
var existingListForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == input.ListFormCode); var existingListForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == input.ListFormCode);
if (existingListForm != null) if (existingListForm != null)
{ {
@ -354,115 +339,27 @@ public class ListFormWizardAppService(
var tableColumns = await GetTableColumnNamesAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand); var tableColumns = await GetTableColumnNamesAsync(input.DataSourceCode, input.SelectCommandType, input.SelectCommand);
var isDeleted = tableColumns.Contains("IsDeleted"); var isDeleted = tableColumns.Contains("IsDeleted");
var isCreated = tableColumns.Contains("CreatorId"); var isCreated = tableColumns.Contains("CreatorId");
input.WorkflowDto ??= new WorkflowDto();
input.WorkflowDto.Criteria ??= []; // Ekranin sozlesmesi tek fabrikadan uretilir; seed dosyasina yazilan da veritabanina
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowDto.Criteria); // yazilan da ayni nesnedir.
NormalizeWorkflowCriteriaIds(listFormCode, input.WorkflowDto.Criteria); var listFormDto = input.BuildListForm(isDeleted, isCreated);
var fieldDtos = input.BuildFields();
var workflow = input.WorkflowDto; var workflow = input.WorkflowDto;
await repoListForm.InsertAsync(new ListForm var listForm = new ListForm();
{ listFormDto.ApplyTo(listForm);
ListFormType = ListFormTypeEnum.List, await repoListForm.InsertAsync(listForm, autoSave: true);
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);
// ListFormField - each item in each group becomes a visible field record foreach (var fieldDto in fieldDtos)
var fieldOrder = 0;
var editGroupOrder = 0;
foreach (var group in input.Groups)
{ {
editGroupOrder++; var field = new ListFormField();
var editOrder = 0; fieldDto.ApplyTo(field);
foreach (var item in group.Items) await repoListFormField.InsertAsync(field, autoSave: true);
{ }
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);
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) foreach (var criteria in workflow.Criteria)
@ -496,153 +393,48 @@ public class ListFormWizardAppService(
await _languageTextAppService.ClearRedisCacheAsync(); await _languageTextAppService.ClearRedisCacheAsync();
// Wizard konfigürasyonunu seed dosyasına kaydet // Wizard konfigürasyonunu seed dosyasına kaydet
await SaveWizardSeedFileAsync(input, isDeleted, isCreated, inserted); await SaveWizardSeedFileAsync(input, listFormDto, fieldDtos, 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;
} }
/// <summary> /// <summary>
/// Wizard konfigürasyonunu JSON dosyası olarak kaydeder. /// 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. /// 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. /// 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> /// </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 try
{ {
var outputPath = ResolveWizardSeedOutputPath(); var outputPath = ResolveWizardSeedOutputPath();
Directory.CreateDirectory(outputPath); Directory.CreateDirectory(outputPath);
// Duzenleme bilgisi yalnizca istege aittir; seed dosyasinda yeri yoktur. // Duzenlemede ilk deploy zamani korunur; seeder dosyalari bu damgaya gore siralar.
input.EditFileName = null; if (input.SeededAt == default)
input.SeededAt = Clock.Now;
var seedData = new WizardSeedFileDto var seedData = new WizardSeedFileDto
{ {
Wizard = input, Wizard = input,
ListForm = listForm,
Fields = fields ?? [],
IsDeletedField = isDeletedField, IsDeletedField = isDeletedField,
IsCreatedField = isCreatedField, IsCreatedField = isCreatedField,
InsertedRecords = inserted InsertedRecords = inserted
}; };
var json = JsonSerializer.Serialize(seedData, SeedJsonWriteOptions); var filePath = Path.Combine(outputPath, ResolveWizardSeedFileName(input.WizardName));
// Dosya adı kullanıcı girdisinden türetildiği için geçersiz karakterler ayıklanır. await File.WriteAllTextAsync(filePath, seedData.ToSeedJson());
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);
logger.LogInformation("Seed file saved: {FilePath}", filePath); 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> /// <summary>
/// Custom yolunda component adı ve rota zorunludur. Rota <see cref="ListFormWizardDto.MenuUrl"/> /// 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>) /// üzerinden taşınır ve CustomComponent sözleşmesiyle (<c>CreateUpdateCustomComponentDto.RoutePath</c>)
@ -724,7 +548,7 @@ public class ListFormWizardAppService(
try try
{ {
var json = await File.ReadAllTextAsync(file); var json = await File.ReadAllTextAsync(file);
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, SeedJsonReadOptions); var seed = WizardSeedFileDto.FromSeedJson(json);
result.Add(new WizardFileInfoDto result.Add(new WizardFileInfoDto
{ {
FileName = fileName, FileName = fileName,
@ -735,7 +559,7 @@ public class ListFormWizardAppService(
ComponentKind = seed?.Wizard?.ComponentKind ?? WizardComponentKindEnum.List, ComponentKind = seed?.Wizard?.ComponentKind ?? WizardComponentKindEnum.List,
CustomComponentName = seed?.Wizard?.CustomComponentName ?? string.Empty, CustomComponentName = seed?.Wizard?.CustomComponentName ?? string.Empty,
MenuUrl = seed?.Wizard?.MenuUrl ?? 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 && HasInsertedRecords = seed?.InsertedRecords != null &&
(seed.InsertedRecords.LanguageKeys.Count > 0 || (seed.InsertedRecords.LanguageKeys.Count > 0 ||
seed.InsertedRecords.PermissionGroupNames.Count > 0 || seed.InsertedRecords.PermissionGroupNames.Count > 0 ||
@ -759,7 +583,7 @@ public class ListFormWizardAppService(
var filePath = ResolveWizardSeedFilePath(fileName); var filePath = ResolveWizardSeedFilePath(fileName);
var json = await File.ReadAllTextAsync(filePath); 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ı."); return seed ?? throw new UserFriendlyException("Dosya okunamadı.");
} }
@ -779,7 +603,7 @@ public class ListFormWizardAppService(
var filePath = ResolveWizardSeedFilePath(fileName); var filePath = ResolveWizardSeedFilePath(fileName);
var json = await File.ReadAllTextAsync(filePath); var json = await File.ReadAllTextAsync(filePath);
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, SeedJsonReadOptions); var seed = WizardSeedFileDto.FromSeedJson(json);
if (seed != null) if (seed != null)
await DeleteWizardDataAsync(seed); await DeleteWizardDataAsync(seed);
@ -992,7 +816,7 @@ public class ListFormWizardAppService(
{ {
var filePath = ResolveWizardSeedFilePath(fileName); var filePath = ResolveWizardSeedFilePath(fileName);
var json = await File.ReadAllTextAsync(filePath); var json = await File.ReadAllTextAsync(filePath);
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, SeedJsonReadOptions) var seed = WizardSeedFileDto.FromSeedJson(json)
?? throw new UserFriendlyException("Dosya okunamadı."); ?? throw new UserFriendlyException("Dosya okunamadı.");
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id)); 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", "en": "Select at least one operation or turn generation off",
"tr": "En az bir operasyon seçin ya da üretimi kapatın" "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", "resourceName": "Platform",
"key": "App.SqlQueryManager.CrudEndpointsInactiveNote", "key": "App.SqlQueryManager.CrudEndpointsInactiveNote",

View file

@ -84,30 +84,44 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
return; return;
} }
var jsonFiles = Directory.GetFiles(wizardDataPath, "*.json").OrderBy(f => Path.GetFileName(f)).ToArray(); var jsonFiles = Directory.GetFiles(wizardDataPath, "*.json");
if (jsonFiles.Length == 0) if (jsonFiles.Length == 0)
{ {
_logger.LogInformation("No JSON files found in Seeds/{Scope}/wizard directory, skipping.", scopeFolderName); _logger.LogInformation("No JSON files found in Seeds/{Scope}/wizard directory, skipping.", scopeFolderName);
return; return;
} }
_logger.LogInformation("WizardDataSeeder started. {Count} files to be processed.", jsonFiles.Length); // Dosya adi tarih tasimadigi icin uygulama sirasi dosyanin kendi damgasindan gelir;
// birbirine bagli wizard'lar (once Customers, sonra Tasks) uretildikleri sirayla uygulanir.
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var seedFiles = new List<(string FilePath, WizardSeedFileDto Seed)>();
foreach (var filePath in jsonFiles) foreach (var filePath in jsonFiles)
{ {
try try
{ {
var json = await File.ReadAllTextAsync(filePath); var seed = WizardSeedFileDto.FromSeedJson(await File.ReadAllTextAsync(filePath));
var seedFile = JsonSerializer.Deserialize<WizardSeedFileDto>(json, options); if (seed?.Wizard == null)
if (seedFile?.Wizard == null)
{ {
_logger.LogWarning("Invalid file skipped: {FilePath}", filePath); _logger.LogWarning("Invalid file skipped: {FilePath}", filePath);
continue; 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(); var wizardName = seedFile.Wizard.WizardName?.Trim();
if (string.IsNullOrWhiteSpace(wizardName)) if (string.IsNullOrWhiteSpace(wizardName))
{ {
@ -115,7 +129,12 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
continue; 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); await BackfillMenuParentModuleIdAsync(seedFile.Wizard);
@ -123,10 +142,10 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
// idempotency menü kaydı üzerinden değerlendirilir. // idempotency menü kaydı üzerinden değerlendirilir.
// Menusuz Custom wizard'da ne ListForm ne de menu kaydi olustugu icin // Menusuz Custom wizard'da ne ListForm ne de menu kaydi olustugu icin
// idempotency okuma permission'i uzerinden degerlendirilir. // idempotency okuma permission'i uzerinden degerlendirilir.
var alreadySeeded = seedFile.Wizard.ComponentKind == WizardComponentKindEnum.Custom var alreadySeeded = isCustomComponent
? seedFile.Wizard.CreateMenu ? seedFile.Wizard.CreateMenu
? await _repoMenu.AnyAsync(a => a.Code == ResolveMenuCode(seedFile.Wizard)) ? await _repoMenu.AnyAsync(a => a.Code == seedFile.Wizard.ResolveMenuCode())
: await _repoPerm.AnyAsync(a => a.Name == ResolveMenuCode(seedFile.Wizard)) : await _repoPerm.AnyAsync(a => a.Name == seedFile.Wizard.ResolveMenuCode())
: await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode); : await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode);
if (alreadySeeded) if (alreadySeeded)
@ -149,23 +168,10 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
private async Task ApplyWizardSeedAsync(WizardSeedFileDto seedFile) private async Task ApplyWizardSeedAsync(WizardSeedFileDto seedFile)
{ {
var input = seedFile.Wizard; var input = seedFile.Wizard;
var isDeleted = seedFile.IsDeletedField; input.Groups ??= [];
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;
var wizardName = input.WizardName.Trim(); var code = input.ResolveMenuCode();
var code = string.IsNullOrWhiteSpace(input.MenuCode) var listFormCode = input.ResolveListFormCode();
? WizardConsts.WizardKey(wizardName)
: input.MenuCode.Trim();
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
? code
: input.ListFormCode.Trim();
NormalizeWorkflowCriteriaIds(listFormCode, workflow.Criteria);
var titleLangKey = $"{listFormCode}.Title"; var titleLangKey = $"{listFormCode}.Title";
var nameLangKey = code; var nameLangKey = code;
var descLangKey = $"{listFormCode}.Desc"; var descLangKey = $"{listFormCode}.Desc";
@ -344,20 +350,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
}, autoSave: true); }, autoSave: true);
} }
// EditingFormJson // ListForm ve alanlari - varsa sil, seed dosyasindaki sozlesmeden yeniden kur
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
var existingListForm = await _repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == input.ListFormCode); var existingListForm = await _repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == input.ListFormCode);
if (existingListForm != null) if (existingListForm != null)
{ {
@ -376,170 +369,26 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
await _repoListFormWorkflow.DeleteManyAsync(existingWorkflowCriteria, autoSave: true); await _repoListFormWorkflow.DeleteManyAsync(existingWorkflowCriteria, autoSave: true);
} }
// EditingForm'da required olan alanları tespit et, ValidationRuleJson eklemek için // Ekran, seed dosyasinin tasidigi ListForm sozlesmesinden kurulur; boylece wizard
var editingFormFieldNames = input.Groups // sonrasinda tasarimci uzerinde yapilan degisiklikler de geri gelir.
.SelectMany(g => g.Items) var listForm = new ListForm();
.Where(i => i.IncludeInEditingForm && i.FieldName != input.KeyFieldName) seedFile.ListForm.ApplyTo(listForm);
.Select(i => new { FieldName = i.FieldName, Isrequired = i.IsRequired }) await _repoListForm.InsertAsync(listForm, autoSave: true);
.ToList();
// Heigth ve Width hesaplama foreach (var fieldDto in seedFile.Fields)
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
{ {
ListFormType = ListFormTypeEnum.List, var field = new ListFormField();
PageSize = 10, fieldDto.ApplyTo(field);
ExportJson = WizardConsts.DefaultExportJson, await _repoListFormField.InsertAsync(field, autoSave: true);
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);
}
} }
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)) 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> /// <summary>
/// Custom yolunda menunun acacagi rotayi normalize eder. Rota MenuUrl uzerinden /// Custom yolunda menunun acacagi rotayi normalize eder. Rota MenuUrl uzerinden
/// tasinir; bos ise menu hicbir yere gitmeyecegi icin seed dosyasi hatali kabul edilir. /// tasinir; bos ise menu hicbir yere gitmeyecegi icin seed dosyasi hatali kabul edilir.
@ -647,119 +485,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
menuParent.ModuleId); 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) 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;
using System.Data; using System.Data;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms;
using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities;
namespace Sozsoft.Platform.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 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" public string CultureName { get; set; } // Bu tanim hangi dil icin "en", "tr"

View file

@ -1,11 +1,12 @@
using System; using System;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Data; using System.Data;
using Sozsoft.Platform.ListForms;
using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities;
namespace Sozsoft.Platform.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 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) 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. - **`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 İ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. 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) - **Yeni bir yetki**`PlatformConsts.AppCodes` + `PermissionsData.json` + (UI'da kullanılacaksa)
`permission.constant.ts` + `LanguagesData.json`. `permission.constant.ts` + `LanguagesData.json`.
- **Yeni bir dil anahtarı**`LanguagesData.json`; eklemeden önce anahtarın zaten var olup - **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 { import {
GanttOptionDto, GanttOptionDto,
GridOptionsEditDto,
SchedulerOptionDto, SchedulerOptionDto,
SubFormDto, SubFormDto,
TodoOptionDto, TodoOptionDto,
@ -43,6 +45,11 @@ export interface ListFormWizardDto {
editFileName?: string editFileName?: string
wizardName: 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 listFormCode: string
menuCode: string menuCode: string
menuOrder: number menuOrder: number
@ -168,6 +175,10 @@ export interface WizardSeedFileWizardDto extends Omit<ListFormWizardDto, 'groups
export interface WizardSeedFileDto { export interface WizardSeedFileDto {
wizard: WizardSeedFileWizardDto 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 isDeletedField: boolean
isCreatedField: boolean isCreatedField: boolean
insertedRecords: WizardInsertedRecordsDto insertedRecords: WizardInsertedRecordsDto

View file

@ -27,7 +27,6 @@ import {
ColumnFormatEditDto, ColumnFormatEditDto,
CopyListFormFieldColumnDto, CopyListFormFieldColumnDto,
} from '@/proxy/admin/list-form-field/models' } from '@/proxy/admin/list-form-field/models'
import { LanguageInfo } from '@/proxy/config/models'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
import { useStoreState } from '@/store' import { useStoreState } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization' 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 { number, object, string } from 'yup'
import FormFieldEdit from './FormFieldEdit' import FormFieldEdit from './FormFieldEdit'
import { dbSourceTypeOptions, sqlDataTypeToDbType } from '../options' import { dbSourceTypeOptions, sqlDataTypeToDbType } from '../options'
import { IdentityRoleDto, IdentityUserDto } from '@/proxy/admin/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { EditingFormDto } from '@/proxy/form/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 { useEffect, useMemo, useState } from 'react'
import CreatableSelect from 'react-select/creatable' import CreatableSelect from 'react-select/creatable'
import { import {
FaArrowDown,
FaArrowLeft, FaArrowLeft,
FaArrowRight, FaArrowRight,
FaArrowUp,
FaChevronDown, FaChevronDown,
FaChevronRight, FaChevronRight,
FaEdit, FaEdit,
@ -47,6 +49,14 @@ interface TreeNodeProps {
saving: boolean saving: boolean
onStartEdit: (node: MenuTreeNode & { id?: string }) => void onStartEdit: (node: MenuTreeNode & { id?: string }) => void
onDelete: (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({ function TreeNode({
@ -59,6 +69,9 @@ function TreeNode({
saving, saving,
onStartEdit, onStartEdit,
onDelete, onDelete,
siblings,
index,
onMove,
}: TreeNodeProps) { }: TreeNodeProps) {
const hasChildren = node.children.length > 0 const hasChildren = node.children.length > 0
const isExpanded = expanded.has(node.code) const isExpanded = expanded.has(node.code)
@ -114,6 +127,34 @@ function TreeNode({
</span> </span>
<span className="shrink-0 flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity"> <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 <Button
type="button" type="button"
disabled={saving} disabled={saving}
@ -146,7 +187,7 @@ function TreeNode({
</div> </div>
{isExpanded && {isExpanded &&
node.children.map((child) => ( node.children.map((child, childIndex) => (
<TreeNode <TreeNode
key={child.code} key={child.code}
node={child as MenuTreeNode & { id?: string }} node={child as MenuTreeNode & { id?: string }}
@ -158,6 +199,9 @@ function TreeNode({
saving={saving} saving={saving}
onStartEdit={onStartEdit} onStartEdit={onStartEdit}
onDelete={onDelete} onDelete={onDelete}
siblings={node.children as (MenuTreeNode & { id?: string })[]}
index={childIndex}
onMove={onMove}
/> />
))} ))}
</div> </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. // Menü ağacı her render'da yeniden dolaşılmasın; id eşlemesi tek haritadan yapılır.
const enrichedNodes = useMemo(() => { const enrichedNodes = useMemo(() => {
const idByCode = new Map(rawItems.map((item) => [item.code, item.id])) const idByCode = new Map(rawItems.map((item) => [item.code, item.id]))
@ -254,6 +342,7 @@ function MenuTreeInline({
saving, saving,
onStartEdit: onEditMenu, onStartEdit: onEditMenu,
onDelete: handleDelete, onDelete: handleDelete,
onMove: handleMove,
} }
return ( return (
@ -269,13 +358,15 @@ function MenuTreeInline({
{translate('::App.WizardStep1.NoMenusAvailable')} {translate('::App.WizardStep1.NoMenusAvailable')}
</div> </div>
) : ( ) : (
enrichedNodes.map((node) => ( enrichedNodes.map((node, index) => (
<TreeNode <TreeNode
key={node.code} key={node.code}
node={node} node={node}
depth={0} depth={0}
selectedCode={value} selectedCode={value}
onSelect={onChange} onSelect={onChange}
siblings={enrichedNodes}
index={index}
{...sharedNodeProps} {...sharedNodeProps}
/> />
)) ))

View file

@ -24,11 +24,14 @@ import {
FaTasks, FaTasks,
FaChartBar, FaChartBar,
FaRegCalendarAlt, FaRegCalendarAlt,
FaToggleOn,
FaToggleOff,
} from 'react-icons/fa' } from 'react-icons/fa'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { getMenus } from '@/services/menu.service' import { getMenus } from '@/services/menu.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { CascadeBehavior, SqlTableRelation, RelationshipType } from '@/proxy/developerKit/models' import { CascadeBehavior, SqlTableRelation, RelationshipType } from '@/proxy/developerKit/models'
import type { CrudEndpoint } from '@/proxy/developerKit/models'
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
import { MenuItem } from '@/proxy/menus/menu' import { MenuItem } from '@/proxy/menus/menu'
import { import {
@ -45,6 +48,7 @@ import {
CRUD_ENDPOINT_PERMISSION, CRUD_ENDPOINT_PERMISSION,
CRUD_OPERATION_TYPES, CRUD_OPERATION_TYPES,
type CrudOperationType, type CrudOperationType,
METHOD_COLOR,
getErrorMessage, getErrorMessage,
getOperationMeta, getOperationMeta,
toEntityName, toEntityName,
@ -1333,6 +1337,9 @@ const SqlTableDesignerDialog = ({
const [crudOperations, setCrudOperations] = useState<CrudOperationType[]>([ const [crudOperations, setCrudOperations] = useState<CrudOperationType[]>([
...CRUD_OPERATION_TYPES, ...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 [columns, setColumns] = useState<ColumnDefinition[]>([createEmptyColumn()])
const [originalColumns, setOriginalColumns] = useState<ColumnDefinition[]>([]) const [originalColumns, setOriginalColumns] = useState<ColumnDefinition[]>([])
const [colsLoading, setColsLoading] = useState(false) const [colsLoading, setColsLoading] = useState(false)
@ -1418,6 +1425,24 @@ const SqlTableDesignerDialog = ({
.catch(() => {}) .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 // Edit mode: load table's existing columns + FK constraints
if (initialTableData && dataSource) { if (initialTableData && dataSource) {
setColsLoading(true) setColsLoading(true)
@ -1541,7 +1566,7 @@ const SqlTableDesignerDialog = ({
.catch(() => {}) .catch(() => {})
.finally(() => setIndexesLoading(false)) .finally(() => setIndexesLoading(false))
} }
}, [isOpen, dataSource, initialTableData]) }, [isOpen, dataSource, initialTableData, canManageCrudEndpoints])
const generatedSql = useMemo( const generatedSql = useMemo(
() => () =>
@ -2126,6 +2151,7 @@ const SqlTableDesignerDialog = ({
setTemplatesOpen(false) setTemplatesOpen(false)
setGenerateCrud(false) setGenerateCrud(false)
setCrudOperations([...CRUD_OPERATION_TYPES]) setCrudOperations([...CRUD_OPERATION_TYPES])
setExistingCrudEndpoints([])
onClose() onClose()
} }
@ -3343,6 +3369,43 @@ const SqlTableDesignerDialog = ({
const renderCrudEndpoints = () => ( const renderCrudEndpoints = () => (
<div className="flex flex-col gap-4"> <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"> <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)} /> <Checkbox checked={generateCrud} onChange={(checked) => setGenerateCrud(checked)} />
<span> <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 getGanttTreeList = (gantt: GanttInstance) => {
const element = (gantt.element() as HTMLElement).querySelector('.dx-treelist') const element = (gantt.element() as HTMLElement).querySelector('.dx-treelist')
return element return element
@ -89,7 +96,7 @@ const GanttView = (props: GanttViewProps) => {
const [columnData, setColumnData] = useState<GridColumnData[]>() const [columnData, setColumnData] = useState<GridColumnData[]>()
const gridDto = useListFormGridDto(listFormCode, extGridDto) const gridDto = useListFormGridDto(listFormCode, extGridDto)
const [scaleType, setScaleType] = useState<GanttScaleType>('weeks') 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 layout = layoutTypes.gantt || 'gantt'
const pendingStateRef = useRef<Record<string, any> | undefined>(undefined) const pendingStateRef = useRef<Record<string, any> | undefined>(undefined)
const loadedStateKeyRef = useRef('') const loadedStateKeyRef = useRef('')
@ -171,11 +178,47 @@ const GanttView = (props: GanttViewProps) => {
return (columnData ?? []) return (columnData ?? [])
.filter((column: any) => column.type !== 'buttons') .filter((column: any) => column.type !== 'buttons')
.map((column: any) => .map((column: any) => ({
chartFields.has(column.dataField) ? { ...column, visible: false } : column, ...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]) }, [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(() => setColumnData(memoizedColumns), [memoizedColumns])
useEffect(() => setGanttDataSource(memoizedDataSource), [memoizedDataSource]) useEffect(() => setGanttDataSource(memoizedDataSource), [memoizedDataSource])
@ -274,7 +317,10 @@ const GanttView = (props: GanttViewProps) => {
treeList.state(treeListState ?? treeState) treeList.state(treeListState ?? treeState)
if (savedScaleType) setScaleType(savedScaleType) if (savedScaleType) setScaleType(savedScaleType)
if (savedTaskListWidth) setTaskListWidth(savedTaskListWidth) if (savedTaskListWidth) {
taskListWidthLockedRef.current = true
setTaskListWidth(savedTaskListWidth)
}
gantt.option({ gantt.option({
...(showDependencies !== undefined ? { showDependencies } : {}), ...(showDependencies !== undefined ? { showDependencies } : {}),
...(showResources !== undefined ? { showResources } : {}), ...(showResources !== undefined ? { showResources } : {}),
@ -282,8 +328,39 @@ const GanttView = (props: GanttViewProps) => {
}) })
}, []) }, [])
const splitterBoundRef = useRef(false)
const columnResizingModeRef = useRef(false)
const handleGanttContentReady = useCallback( const handleGanttContentReady = useCallback(
(event: ContentReadyEvent) => { (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 if (!pendingStateRef.current) return
const state = pendingStateRef.current const state = pendingStateRef.current
@ -309,8 +386,9 @@ const GanttView = (props: GanttViewProps) => {
customizationData: '', customizationData: '',
}) })
getGanttTreeList(gantt)?.state(null) getGanttTreeList(gantt)?.state(null)
setTaskListWidth(500) taskListWidthLockedRef.current = false
}, [listFormCode, storageKey]) setTaskListWidth(defaultTaskListWidth)
}, [defaultTaskListWidth, listFormCode, storageKey])
useEffect(() => { useEffect(() => {
if ( if (