WizardFileManager önce Json sonra DbMigrate
This commit is contained in:
parent
246dc17fcb
commit
5560d3bb8c
31 changed files with 1789 additions and 1272 deletions
15
.github/instructions/ai.instructions.md
vendored
15
.github/instructions/ai.instructions.md
vendored
|
|
@ -985,11 +985,16 @@ covered by `RollbackImport`, which only restores files and must be run before it
|
||||||
`CompleteImport` runs in **dependency order and the order is fixed**: (a) the `.sql` scripts under
|
`CompleteImport` runs in **dependency order and the order is fixed**: (a) the `.sql` scripts under
|
||||||
`{sql|postgres}/{object,execute}` (`object` first) — a missing table is created here, otherwise the
|
`{sql|postgres}/{object,execute}` (`object` first) — a missing table is created here, otherwise the
|
||||||
later steps would fail on it; (b) columns missing from existing tables are added; (c) `crud/*.json`
|
later steps would fail on it; (b) columns missing from existing tables are added; (c) `crud/*.json`
|
||||||
through `ICrudEndpointSeedApplier`; (d) the `data/*.json` rows; (e) `wizard/*.json` — the screens
|
through `ICrudEndpointSeedApplier`; (d) the `data/*.json` rows, except the platform files that hold
|
||||||
are installed through the same path the wizard screen uses (`DeployWizardAsync`), reusing the file's
|
the wizards' menu/permission/language rows (`WizardSeedRowStore.ListFormCodes`); (e) `wizard/*.json`
|
||||||
`ListForm`/`Fields` contract as-is instead of rebuilding it from the wizard answers. A screen that
|
— the screens are **not** installed: like a deploy, the file is marked `Pending` and the next DB
|
||||||
already exists is left untouched, and each screen is installed in its own unit of work. No migrate
|
Migrate installs it (`WizardDataSeeder`) from the file's `ListForm`/`Fields` contract as-is, even
|
||||||
run is needed: the screen works as soon as the import finishes.
|
when the target already has the screen.
|
||||||
|
|
||||||
|
A wizard deploy (wizard screen, MCP `deploy_wizard`, edit) never writes to the database: it only
|
||||||
|
writes seed files — `wizard/{Name}.json` plus the menu, module, permission group, permission and
|
||||||
|
language rows in the platform `data/*.json` files. DB Migrate is the only installer, and it never
|
||||||
|
grants the permissions to a role (admin included); roles get them from the role screen.
|
||||||
|
|
||||||
Limits: allowed root folders `wizard`, `crud`, `data`, `sql`, `postgres` (and only `object` /
|
Limits: allowed root folders `wizard`, `crud`, `data`, `sql`, `postgres` (and only `object` /
|
||||||
`execute` under the SQL provider folders); 5 MB per entry, 50 MB per archive, 500 entries.
|
`execute` under the SQL provider folders); 5 MB per entry, 50 MB per archive, 500 entries.
|
||||||
|
|
|
||||||
137
.github/instructions/lowcode.instructions.md
vendored
137
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -68,7 +68,7 @@ Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen ayn
|
||||||
| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı. MCP'de yeni tablo `create_table` ile kurulur (script ve dosya tasarımcıyla aynı); view MCP'den kurulamaz |
|
| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı. MCP'de yeni tablo `create_table` ile kurulur (script ve dosya tasarımcıyla aynı); view MCP'den kurulamaz |
|
||||||
| 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) | `data/App.DeveloperKit.CustomComponents/{Id}.json` | Wizard `Custom` yolunda bileşen adını arar |
|
| 3 | Custom Component (gerekiyorsa) | `data/App.DeveloperKit.CustomComponents/{Id}.json` | Wizard `Custom` yolunda bileşen adını arar |
|
||||||
| 4 | Wizard | `wizard/{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir. List ekranı **deploy** ile kurulur (MCP `deploy_wizard` ya da wizard ekranı, §0.10); dosyayı sunucu yazar. Elle yazılan List dosyası `ListForm` + `Fields` taşımak zorundadır, yoksa seeder atlar (§4.1) |
|
| 4 | Wizard | `wizard/{Ad}.json` + menü/yetki/dil satırları (`data/App.Menus.*`, `data/Abp.Identity.Permission*`, `data/App.Languages.LanguageText.json`) | Deploy (MCP `deploy_wizard` ya da wizard ekranı, §0.10) **yalnızca bu dosyaları yazar**; ekran, menü ve yetkiler DB Migrate ile kurulur (§4.0). Dosyaları sunucu yazar. Elle yazılan List dosyası `ListForm` + `Fields` taşımak zorundadır, yoksa seeder atlar (§4.1) |
|
||||||
| 5 | Alt ekranlar (varsa) | ek `wizard/…` dosyaları | Ana wizard'dan **önceki** `SeededAt` damgasını alır |
|
| 5 | Alt ekranlar (varsa) | ek `wizard/…` dosyaları | Ana wizard'dan **önceki** `SeededAt` damgasını alır |
|
||||||
| 6 | Liste verisi (isteniyorsa) | `data/{ListFormCode}.json` | Ekran ve tablo hazır olmalı; `ListForm.SeedFilePath` bu yolu gösterir (§9.15) |
|
| 6 | Liste verisi (isteniyorsa) | `data/{ListFormCode}.json` | Ekran ve tablo hazır olmalı; `ListForm.SeedFilePath` bu yolu gösterir (§9.15) |
|
||||||
| 7 | SEO alanları (public yol ürettiysen) | Yolu tanımlayan satırın kendisi: `data/App.Menus.Routes.json` ya da `data/App.DeveloperKit.CustomComponents/{Id}.json` | Yolu üreten dosya yazıldıktan sonra; ziyaretçiye açık hiçbir yol metasız kalmaz (§0.9) |
|
| 7 | SEO alanları (public yol ürettiysen) | Yolu tanımlayan satırın kendisi: `data/App.Menus.Routes.json` ya da `data/App.DeveloperKit.CustomComponents/{Id}.json` | Yolu üreten dosya yazıldıktan sonra; ziyaretçiye açık hiçbir yol metasız kalmaz (§0.9) |
|
||||||
|
|
@ -503,13 +503,13 @@ ister ve kiracı kullanıcısına hiçbiri açılmaz.
|
||||||
| `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla istenir (`4.3`); aynı numarayı iki başlık taşımamalıdır |
|
| `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla istenir (`4.3`); aynı numarayı iki başlık taşımamalıdır |
|
||||||
| `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz |
|
| `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz |
|
||||||
| `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` |
|
| `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` |
|
||||||
| `deploy_wizard` | Wizard cevabından ekranı, menüyü, yetkileri, dil anahtarlarını ve seed dosyasını üretir | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). İstek demo veri taşıyorsa `SeedData: true` (§0.10.1) |
|
| `deploy_wizard` | Wizard cevabından ekranın wizard dosyasını ve menü/yetki/dil seed satırlarını yazar; veritabanına yazmaz (§4.0) | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). İstek demo veri taşıyorsa `SeedData: true` (§0.10.1). Çıkıştaki `isPending: true` ekranın henüz kurulmadığını söyler — kullanıcı Wizard Manager'da DB Migrate çalıştırır |
|
||||||
| `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) |
|
| `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler; `dropTables: true` ekranın tablosunu da düşürür | Bağımlı wizard varsa reddeder. Tablo kullanıcı açıkça onaylamadan düşürülmez; `App.SqlQueryManager` ister (§4.5) |
|
||||||
| `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) |
|
| `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) |
|
||||||
| `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + resim içerikleri) | Yarım kalan talebe dönmek için; `App.DevRequests` ister |
|
| `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + resim içerikleri) | Yarım kalan talebe dönmek için; `App.DevRequests` ister |
|
||||||
| `append_dev_request_note` | Talep açıklamasının sonuna HTML ekler | Sonuç notu için; `Description`'ı `update_row` ile yazma (§0.11) |
|
| `append_dev_request_note` | Talep açıklamasının sonuna HTML ekler | Sonuç notu için; `Description`'ı `update_row` ile yazma (§0.11) |
|
||||||
| `add_dev_request_subtasks` | `SubTasks` listesine tamamlanmış satırlar ekler | Atanan = bağlanan kullanıcı, bitiş = bugün (sunucu yazar); `SubTasks`'ı `update_row` ile yazma (§0.11) |
|
| `add_dev_request_subtasks` | `SubTasks` listesine tamamlanmış satırlar ekler | Atanan = bağlanan kullanıcı, bitiş = bugün (sunucu yazar); `SubTasks`'ı `update_row` ile yazma (§0.11) |
|
||||||
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü, yetki tanımı, ekran sorgusu (List) ya da bileşen (Custom), seed kapanışı | `passed` yalnızca engelleyici bir kontrol kalırsa `false`; `seed_closure` engellemez ama rapora yazılır (§0.11) |
|
| `verify_wizard` | DB Migrate ile kurulmuş ekranı veri yazmadan doğrular (migrate bekleyen dosyada `install` kontrolü başarısız döner, diğerleri yapılmaz): menü, yetki tanımı, ekran sorgusu (List) ya da bileşen (Custom), seed kapanışı | `passed` yalnızca engelleyici bir kontrol kalırsa `false`; `seed_closure` engellemez ama rapora yazılır (§0.11) |
|
||||||
| `list_components` · `get_component` · `save_component` · `delete_component` | Custom component okuma/yazma (ada göre upsert) | `save_component` visual designer bileşenini reddeder — yalnız Code modu (§7.1.1) |
|
| `list_components` · `get_component` · `save_component` · `delete_component` | Custom component okuma/yazma (ada göre upsert) | `save_component` visual designer bileşenini reddeder — yalnız Code modu (§7.1.1) |
|
||||||
|
|
||||||
**Yetkiler.** MCP'nin iki yetkisi vardır: `.Invoke` (bağlantı + okuma) ve `.Write` (her
|
**Yetkiler.** MCP'nin iki yetkisi vardır: `.Invoke` (bağlantı + okuma) ve `.Write` (her
|
||||||
|
|
@ -526,7 +526,9 @@ eklenen dosya) bu satırlar ekranla birlikte seed edilir; migrate çalıştığ
|
||||||
|
|
||||||
1. `deploy_wizard` cevabına `SeedData: true` yaz. Ekranın `SeedFilePath`'i
|
1. `deploy_wizard` cevabına `SeedData: true` yaz. Ekranın `SeedFilePath`'i
|
||||||
`data/{ListFormCode}.json` olur, `SeedSyncInsert/Update/Delete` açılır (§9.15).
|
`data/{ListFormCode}.json` olur, `SeedSyncInsert/Update/Delete` açılır (§9.15).
|
||||||
2. Deploy'dan sonra her satırı `insert_row` ile ekle. Satır ekranın insert sözleşmesinden geçer
|
2. Deploy ekranı kurmaz; kullanıcıdan Wizard Manager'da **DB Migrate** çalıştırmasını iste.
|
||||||
|
`list_wizards`'ta dosyanın `isPending` değeri `false` olunca (ya da `verify_wizard` geçince)
|
||||||
|
her satırı `insert_row` ile ekle. Satır ekranın insert sözleşmesinden geçer
|
||||||
ve `ListFormSeedDataSynchronizer` onu `configs/seeds/host/data/{ListFormCode}.json` dosyasına
|
ve `ListFormSeedDataSynchronizer` onu `configs/seeds/host/data/{ListFormCode}.json` dosyasına
|
||||||
yazar (kiracı bağlamında `tenants/{TenantId}/data/`).
|
yazar (kiracı bağlamında `tenants/{TenantId}/data/`).
|
||||||
3. Migrate sırasında wizard seeder ekranı kurar, ardından `ListFormSeedDataApplier` dosyadaki
|
3. Migrate sırasında wizard seeder ekranı kurar, ardından `ListFormSeedDataApplier` dosyadaki
|
||||||
|
|
@ -768,7 +770,7 @@ 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/{WizardName}.json` (dosya adında tarih yoktur)
|
Konum: `configs/seeds/{kapsam}/wizard/{WizardName}.json` (dosya adında tarih yoktur)
|
||||||
Okuyan: `WizardDataSeeder` · Yazan: `ListFormWizardAppService` (deploy) ve `WizardSeedSynchronizer`
|
Okuyan: `WizardDataSeeder` (migrate) · Yazan: `ListFormWizardAppService` (deploy) ve `WizardSeedSynchronizer`
|
||||||
(ListForm editöründen kayıt) · Okuma/yazma her zaman `WizardSeedFileDto.FromSeedJson` /
|
(ListForm editöründen kayıt) · Okuma/yazma her zaman `WizardSeedFileDto.FromSeedJson` /
|
||||||
`ToSeedJson` ile — düz `JsonSerializer` bloklu bölümleri düşürür.
|
`ToSeedJson` ile — düz `JsonSerializer` bloklu bölümleri düşürür.
|
||||||
|
|
||||||
|
|
@ -777,6 +779,46 @@ girdisidir — wizard ekranı ve MCP `deploy_wizard` bunu alır; *seed dosyası*
|
||||||
çıktısıdır. List yolunda dosyayı deploy'a ürettir; `ListForm` bölümü olmayan List dosyası
|
çıktısıdır. List yolunda dosyayı deploy'a ürettir; `ListForm` bölümü olmayan List dosyası
|
||||||
seeder tarafından uyarıyla **atlanır**.
|
seeder tarafından uyarıyla **atlanır**.
|
||||||
|
|
||||||
|
### 4.0 Deploy dosya yazar, migrate kurar
|
||||||
|
|
||||||
|
Deploy (wizard ekranı, MCP `deploy_wizard`, düzenleme dahil) **veritabanına hiçbir şey yazmaz ve
|
||||||
|
hiçbir şey silmez**. Tek çıktısı seed dosyalarıdır:
|
||||||
|
|
||||||
|
| Dosya | İçerik |
|
||||||
|
| --- | --- |
|
||||||
|
| `wizard/{WizardName}.json` | Ekranın `ListForm`/`Fields` sözleşmesi, wizard cevapları, `InsertedRecords`, `Pending` |
|
||||||
|
| `data/App.Menus.Module.json`, `data/App.Menus.Menu.json` | Modül, üst menü, ekranın menüsü |
|
||||||
|
| `data/Abp.Identity.PermissionGroups.json`, `data/Abp.Identity.Permissions.json` | İzin grubu, okuma yetkisi ve alt yetkileri |
|
||||||
|
| `data/App.Languages.LanguageText.json` | Menü, başlık, açıklama, grup ve alan başlıklarının EN + TR metinleri (`Id` iş anahtarından v5 GUID, `SeedRowIds.LanguageText`) |
|
||||||
|
|
||||||
|
Satırları `WizardSeedRowStore` yazar; anahtarlar `WizardSeedRowKeys`'ten gelir (export aynı
|
||||||
|
listeyle toplar). **Sahiplik:** bir satır ne veritabanında ne dosyada varsa ya da önceki sürüm ona
|
||||||
|
sahipse wizard'ındır (`InsertedRecords`) ve cevaplarla yazılır; başkasına ait satıra (var olan üst
|
||||||
|
menü, izin grubu, paylaşılan dil anahtarı) dokunulmaz — metni kendi ekranından değişir.
|
||||||
|
|
||||||
|
Kurulum **DB Migrate**'tedir (Wizard Manager'daki buton, `POST /api/setup/migrate`). Sıra:
|
||||||
|
`WizardDataSeeder` ekrana özgü kayıtları kurar (veri kaynağı, ListForm, alanlar, iş akışı), ardından
|
||||||
|
`ListFormSeedDataApplier` menü/yetki/dil satırlarını ekler (eksik olanı ekler, var olana dokunmaz).
|
||||||
|
**Yetkiler hiçbir role otomatik atanmaz** — admin dahil; ekranı hangi rolün göreceği rol ekranından
|
||||||
|
seçilir.
|
||||||
|
|
||||||
|
Deploy'dan migrate'e kadar wizard'ın hiçbir kaydı veritabanında yoktur ve bu beklenen durumdur.
|
||||||
|
Yeni modül, kök/üst menü, izin grubu, yetki ya da dil anahtarı veritabanında bulunmuyorsa
|
||||||
|
deploy onu dosyaya yazar, migrate `Order` sırasıyla (route → modül → menü, grup → yetki) ekler.
|
||||||
|
Bunları uyarı olarak raporlama, kullanıcıdan başka bir ekrandan eklemesini isteme. Deploy
|
||||||
|
sonrası kullanıcıya söylenecek tek adım **DB Migrate**'tir.
|
||||||
|
|
||||||
|
**Bekleyen kurulum (`Pending`).** Deploy dosyaya `Pending` bölümü yazar; Wizard Manager dosyayı
|
||||||
|
"Migrate bekliyor" olarak gösterir (`WizardFileInfoDto.IsPending` = `Pending` dolu **ya da** ekran bu
|
||||||
|
veritabanında kurulu değil). Düzenleme önceki sürümün sahip olduğu kayıtları
|
||||||
|
`Pending.Replaced`'a, kodu değişen eski ekranı `Pending.ListFormCodes`'a ekler ve artık üretilmeyen
|
||||||
|
satırları dosyalardan çıkarır. Migrate bekleyen dosyada önce bu kayıtları ve wizard'ın güncel
|
||||||
|
kayıtlarını veritabanından kaldırır, ekranı yeniden kurar, `Pending`'i siler; kaldırılan satırları
|
||||||
|
applier dosyadaki güncel hâlleriyle geri ekler. "Yalnızca eksiği ekle" kuralı böylece düzenlenmiş
|
||||||
|
ekranı da güncel hâline getirir. Kaldırılan yetkinin rol atamaları yalnızca yetki kalıcı olarak
|
||||||
|
gidiyorsa silinir. `WizardSeedSynchronizer` bekleyen dosyaya yazmaz — dosya ekranın bir sonraki
|
||||||
|
sürümünü taşır.
|
||||||
|
|
||||||
### 4.1 Dosya iskeleti
|
### 4.1 Dosya iskeleti
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
|
|
@ -859,7 +901,7 @@ saklanır. Dosya okunurken bu alanlar `ListForm`/`Fields` üzerinden geri doldur
|
||||||
| `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ı. |
|
||||||
| `EditFileName` | Yalnızca **istek girdisi** (MCP'de `replaceFileName`): doluysa güncellemedir — `.Update` yetkisi ister, `SeededAt` korunur, eski dosya ve ürettiği kayıtlar `Redeploy` kipinde silinip ekran **cevaptan yeniden üretilir**; ListForm editöründe yapılmış, wizard'da karşılığı olmayan ayarlar kaybolur. Seed dosyasına yazılmaz. |
|
| `EditFileName` | Yalnızca **istek girdisi** (MCP'de `replaceFileName`): doluysa güncellemedir — `.Update` yetkisi ister, `SeededAt` korunur, dosyalar cevaptan yeniden yazılır ve ekran migrate'te **cevaptan yeniden kurulur** (§4.0); ListForm editöründe yapılmış, wizard'da karşılığı olmayan ayarlar kaybolur. Seed dosyasına yazılmaz. |
|
||||||
|
|
||||||
**Menü**
|
**Menü**
|
||||||
|
|
||||||
|
|
@ -891,8 +933,8 @@ saklanır. Dosya okunurken bu alanlar `ListForm`/`Fields` üzerinden geri doldur
|
||||||
| `Language.MenuParentEn` / `Tr` | Üst menü etiketi. |
|
| `Language.MenuParentEn` / `Tr` | Üst menü etiketi. |
|
||||||
|
|
||||||
Üretilen yetkiler: `{MenuCode}` (okuma, kök) + List yolunda `.Create .Update .Delete .Export
|
Üretilen yetkiler: `{MenuCode}` (okuma, kök) + List yolunda `.Create .Update .Delete .Export
|
||||||
.Import .Note`, Custom yolunda yalnızca `.Create .Update .Delete`. Deploy hepsini admin rolüne
|
.Import .Note`, Custom yolunda yalnızca `.Create .Update .Delete`. Hiçbiri bir role otomatik
|
||||||
grant eder; `WizardDataSeeder` grant yazmaz — migrate ile gelen ekranın rol yetkisi ayrıca verilir.
|
atanmaz (admin dahil); ekranı görecek roller yetkiyi rol ekranından alır.
|
||||||
|
|
||||||
**Veri**
|
**Veri**
|
||||||
|
|
||||||
|
|
@ -1387,10 +1429,11 @@ bölümlerine düşer (§4.1). `SeededAt` ilk deploy anında sunucu tarafından
|
||||||
### 4.4.1 Deploy'un ve seeder'ın senin yerine ürettikleri — cevaba yazma
|
### 4.4.1 Deploy'un ve seeder'ın senin yerine ürettikleri — cevaba yazma
|
||||||
|
|
||||||
Aşağıdakileri deploy wizard cevabından **kendisi** üretir (`ListFormWizardDto.BuildListForm` /
|
Aşağıdakileri deploy wizard cevabından **kendisi** üretir (`ListFormWizardDto.BuildListForm` /
|
||||||
`BuildFields`; menü, yetki, dil ve veri kaynağı kayıtları `ListFormWizardAppService`). Ekrana ait
|
`BuildFields`; menü, yetki ve dil satırları `ListFormWizardAppService` → `WizardSeedRowStore`).
|
||||||
olanlar dosyanın `ListForm`/`Fields` bölümlerine düşer; `WizardDataSeeder` migrate'te ekranı
|
Ekrana ait olanlar dosyanın `ListForm`/`Fields` bölümlerine düşer; `WizardDataSeeder` migrate'te
|
||||||
oradan olduğu gibi kurar (`GridOptionsEditDto.ApplyTo`), yeniden türetmez. Menü, yetki ve dil
|
ekranı oradan olduğu gibi kurar (`GridOptionsEditDto.ApplyTo`), yeniden türetmez. Menü, yetki ve
|
||||||
kayıtlarını seeder `Wizard` bölümünden aynı kurallarla üretir:
|
dil kayıtları platform seed dosyalarına satır olarak yazılır, migrate `ListFormSeedDataApplier`
|
||||||
|
ile ekler (§4.0). Sıra değerleri veritabanı ile dosyadaki satırların birleşiminden hesaplanır:
|
||||||
|
|
||||||
| Üretilen | Kural |
|
| Üretilen | Kural |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|
@ -1399,7 +1442,7 @@ kayıtlarını seeder `Wizard` bölümünden aynı kurallarla üretir:
|
||||||
| Yetkilerin dil anahtarları | Ortak sabitler: `App.Platform.Create/Update/Delete/Export/Import/Note` |
|
| Yetkilerin dil anahtarları | Ortak sabitler: `App.Platform.Create/Update/Delete/Export/Import/Note` |
|
||||||
| Üst menü | `MenuParentCode` yoksa yaratılır, `Order = max(kök Order) + 1` |
|
| Üst menü | `MenuParentCode` yoksa yaratılır, `Order = max(kök Order) + 1` |
|
||||||
| Menü sırası | `MenuOrder` 0/boş ise `max(kardeş Order) + 1` |
|
| Menü sırası | `MenuOrder` 0/boş ise `max(kardeş Order) + 1` |
|
||||||
| `DataSource` | `DataSourceCode` yoksa yaratılır; bağlantı metninde `Server` geçiyorsa `Mssql`, aksi hâlde `Postgresql` |
|
| `DataSource` | `DataSourceCode` yoksa migrate yaratır; bağlantı metninde `Server` geçiyorsa `Mssql`, aksi hâlde `Postgresql` |
|
||||||
| `LayoutJson` | Görünüm bayrakları + `DefaultLayout` |
|
| `LayoutJson` | Görünüm bayrakları + `DefaultLayout` |
|
||||||
| `PermissionJson` / alan `PermissionJson` | `{MenuCode}` kökünden türetilir |
|
| `PermissionJson` / alan `PermissionJson` | `{MenuCode}` kökünden türetilir |
|
||||||
| `EditingFormJson` | `Groups` içinden; anahtar alan ve `IncludeInEditingForm: false` olanlar hariç |
|
| `EditingFormJson` | `Groups` içinden; anahtar alan ve `IncludeInEditingForm: false` olanlar hariç |
|
||||||
|
|
@ -1431,31 +1474,45 @@ kayıtlarını seeder `Wizard` bölümünden aynı kurallarla üretir:
|
||||||
|
|
||||||
### 4.5 Güncelleme, idempotency ve silme semantiği
|
### 4.5 Güncelleme, idempotency ve silme semantiği
|
||||||
|
|
||||||
**Seeder idempotenttir — bir ekran zaten varsa dosya atlanır** (yalnızca eksik dil metinleri
|
**Seeder idempotenttir — bir ekran zaten kuruluysa ve bekleyen kurulumu (`Pending`) yoksa dosya
|
||||||
eklenir). "Zaten var" ölçütü yola göre değişir:
|
atlanır.** "Kurulu" ölçütü yola göre değişir (Wizard Manager'daki durum da aynı karardır):
|
||||||
|
|
||||||
| Yol | Kontrol |
|
| Yol | Kontrol |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `List` | `ListForm` kaydında bu `ListFormCode` var mı |
|
| `List` | `ListForm` kaydında bu `ListFormCode` var mı |
|
||||||
| `Custom` + menülü | Bu `MenuCode` ile menü var mı |
|
| `Custom` | Bu `MenuCode` adında yetki var mı |
|
||||||
| `Custom` + menüsüz | Bu `MenuCode` adında yetki var mı |
|
|
||||||
|
|
||||||
Sonuç: **kurulu bir ekranı seed dosyasını düzenleyerek değiştiremezsin** (dosya yalnızca boş
|
Sonuç: **kurulu bir ekranı seed dosyasını elle düzenleyerek değiştiremezsin** (`Pending` yazan
|
||||||
veritabanında etkilidir). Değiştirmenin yolları:
|
deploy'dur). Değiştirmenin yolları:
|
||||||
|
|
||||||
1. **ListForm editörü** (`/admin/listform/edit/{kod}`) — alan ekleme, JSON kolonları, komutlar,
|
1. **ListForm editörü** (`/admin/listform/edit/{kod}`) — alan ekleme, JSON kolonları, komutlar,
|
||||||
görünümler. Her kayıt `WizardSeedSynchronizer` ile aynı dosyanın `ListForm`/`Fields`/`Wizard`
|
görünümler. Her kayıt `WizardSeedSynchronizer` ile aynı dosyanın `ListForm`/`Fields`/`Wizard`
|
||||||
bölümlerine geri yazılır; **ince ayar için tercih edilen yol** budur.
|
bölümlerine geri yazılır; **ince ayar için tercih edilen yol** budur.
|
||||||
2. **`EditFileName` ile yeniden deploy** (wizard ekranı ya da MCP `replaceFileName`) — menü,
|
2. **`EditFileName` ile yeniden deploy** (wizard ekranı ya da MCP `replaceFileName`) — menü,
|
||||||
yetki grubu, veri kaynağı gibi wizard cevabına ait değişiklikler için. Ekran cevaptan yeniden
|
yetki grubu, veri kaynağı gibi wizard cevabına ait değişiklikler için. Dosyalar yeniden yazılır,
|
||||||
üretilir; editörde yapılmış, wizard'da karşılığı olmayan ayarlar kaybolur.
|
ekran DB Migrate'te cevaptan yeniden kurulur (§4.0); editörde yapılmış, wizard'da karşılığı
|
||||||
|
olmayan ayarlar kaybolur.
|
||||||
3. İlgili kayıtları elle sil, sonra seed'i yeniden çalıştır.
|
3. İlgili kayıtları elle sil, sonra seed'i yeniden çalıştır.
|
||||||
|
|
||||||
> `List` yolunda seeder ekranı uygularken `ListForm`, `ListFormField` ve `ListFormWorkflow`
|
> `List` yolunda seeder ekranı uygularken `ListForm`, `ListFormField` ve `ListFormWorkflow`
|
||||||
> kayıtlarını **silip yeniden yazar**; idempotency kapısını geçen dosya için bu güvenlidir.
|
> kayıtlarını **silip yeniden yazar**; idempotency kapısını geçen dosya için bu güvenlidir.
|
||||||
|
|
||||||
**Silme** (`WizardRecordCleaner`): ekran (`ListForm` + alanlar + iş akışı) ve `InsertedRecords`
|
**Silme** iki yoldur ve ikisinde de **başka bir yerde kullanılan kayıt/satır silinmez**:
|
||||||
içindekiler, **başka bir yerde kullanılmıyorsa** silinir. Sıra bağımlılık yönündedir — önce
|
|
||||||
|
- **Migrate bekleyen wizard** (`IsPending`) — veritabanına dokunulmaz. Yalnızca wizard'ın json
|
||||||
|
dosyalarına eklediği satırlar (`InsertedRecords` + `Pending.Replaced`: menü, modül, yetki, izin
|
||||||
|
grubu, dil metni) çıkarılır (`WizardSeedRowStore.RemoveOwned`). Satır, seed dosyalarında ona
|
||||||
|
bağlanan başka bir satır (alt menü, alt yetki, menünün istediği yetki, gruptaki yetki, modülü
|
||||||
|
kullanan menü/yetki, anahtarı görünen ad olarak kullanan satır) ya da veritabanında onu kullanan
|
||||||
|
bir kayıt varsa (`WizardRecordCleaner.FindUsagesAsync`, aşağıdaki tablo) kalır ve raporda
|
||||||
|
kullananlarıyla görünür.
|
||||||
|
- **Kurulu wizard** — önce veritabanı kayıtları kendi işleminde silinir (`WizardRecordCleaner`);
|
||||||
|
işlem başarıyla tamamlanırsa silinen (ya da veritabanında bulunmayan) kayıtların json satırları
|
||||||
|
çıkarılır (`RemoveSeedRowsAsync`). Veritabanı adımı hata verirse dosyalara dokunulmaz. Migrate
|
||||||
|
yalnızca eksik kaydı ekler, silemez; bu yüzden silme veritabanına doğrudan yansır.
|
||||||
|
|
||||||
|
Kurulu wizard'da ekran (`ListForm` + alanlar + iş akışı) ve `InsertedRecords` içindekiler, **başka
|
||||||
|
bir yerde kullanılmıyorsa** silinir. Sıra bağımlılık yönündedir — önce
|
||||||
kullanan, sonra kullanılan — ve her kontrol o ana kadar kalan kayıtlara karşı yapılır:
|
kullanan, sonra kullanılan — ve her kontrol o ana kadar kalan kayıtlara karşı yapılır:
|
||||||
|
|
||||||
| Kayıt | Silinmez, çünkü kullanan var |
|
| Kayıt | Silinmez, çünkü kullanan var |
|
||||||
|
|
@ -1475,24 +1532,21 @@ olabilir. Üst menü zinciri yukarı doğru yürünür — alt menüsü kalmayan
|
||||||
(`Url` boş) menü silinir, adresi olan ya da altında menü kalan kayıtta zincir durur. Wizard'ın
|
(`Url` boş) menü silinir, adresi olan ya da altında menü kalan kayıtta zincir durur. Wizard'ın
|
||||||
`PermissionGroupName`'i içinde yetki kalmamışsa, silinen menülerin ve `MenuParentModuleId`'nin
|
`PermissionGroupName`'i içinde yetki kalmamışsa, silinen menülerin ve `MenuParentModuleId`'nin
|
||||||
modülü yukarıdaki tabloda kullananı yoksa silinir; adlarının dil anahtarları da aynı kontrolden
|
modülü yukarıdaki tabloda kullananı yoksa silinir; adlarının dil anahtarları da aynı kontrolden
|
||||||
geçer. Bu kaplar rapora yalnızca silindiklerinde girer. Düzenleme (redeploy) yolunda kaplara
|
geçer. Bu kaplar rapora yalnızca silindiklerinde girer.
|
||||||
dokunulmaz.
|
|
||||||
|
|
||||||
Silinen menü, modül, yetki ve yetki grubu satırları ekranların seed dosyalarından da çıkarılır
|
Kurulu wizard'da silinen ya da veritabanında bulunmayan menü, modül, yetki, yetki grubu ve dil
|
||||||
(`App.Menus.{Menu,Module}.json`, `Abp.Identity.Permission{s,Groups}.json`;
|
metni satırları, veritabanı işlemi tamamlandıktan sonra seed dosyalarından da çıkarılır (`App.Menus.{Menu,Module}.json`,
|
||||||
`IListFormSeedDataSynchronizer.RemoveRowsByFieldAsync`, iş anahtarıyla, `SeedSyncDelete`'e
|
`Abp.Identity.Permission{s,Groups}.json`, `App.Languages.LanguageText.json`;
|
||||||
bağlı). Wizard bu dosyalara yazmaz, ama kayıt menü/yetki ekranlarından düzenlendiğinde satır
|
`IListFormSeedDataSynchronizer.RemoveRowsByFieldAsync`, iş anahtarıyla, `SeedSyncDelete`'e bağlı);
|
||||||
senkronizasyonla girer; dosyada kalırsa migrate kaydı geri getirir.
|
dosyada kalırsa migrate kaydı geri getirir. Kullanımda olduğu için bırakılan kaydın satırı kalır.
|
||||||
|
|
||||||
Bağlı wizard'ı olan (alt formu ya da component bağımlılığı wizard olarak duran) bir wizard
|
Bağlı wizard'ı olan (alt formu ya da component bağımlılığı wizard olarak duran) bir wizard
|
||||||
silinemez; önce bağlı olanlar silinir. Kural listedeki gruplamayla aynı kaynaktandır
|
silinemez; önce bağlı olanlar silinir. Kural listedeki gruplamayla aynı kaynaktandır
|
||||||
(`WizardFileInfoDto.Dependencies`), sunucu `DeleteFile`'da reddeder, istemci butonu kapatır.
|
(`WizardFileInfoDto.Dependencies`), sunucu `DeleteFile`'da reddeder, istemci butonu kapatır.
|
||||||
Silme ucu (`DeleteFile`) her kaydın akıbetini (`Deleted` / `Kept` + kullananlar / `NotFound`)
|
Silme ucu (`DeleteFile`) her kaydın akıbetini (`Deleted` / `Kept` + kullananlar / `NotFound`)
|
||||||
`WizardDeleteResultDto` olarak döner ve loglar; Wizard Manager bunu rapor olarak gösterir.
|
`WizardDeleteResultDto` olarak döner ve loglar; Wizard Manager bunu rapor olarak gösterir.
|
||||||
Düzenleme yolu (`EditFileName`) aynı sınıfı `Redeploy` kipinde çağırır: üst menü, yetki grubu,
|
Düzenleme (`EditFileName`) bu sınıfı çağırmaz; önceki sürümün kayıtlarını migrate kaldırır (§4.0).
|
||||||
modül, veri kaynağı ve admin dışındaki rollerin yetki atamaları korunur — hemen ardından yeniden
|
Wizard'ın yarattığı modül `InsertedRecords.ModuleIds` ile izlenir.
|
||||||
kullanılırlar; silinip üretilseler menü sırası ve rol atamaları kaybolurdu. Wizard'ın yarattığı
|
|
||||||
modül `InsertedRecords.ModuleIds` ile izlenir.
|
|
||||||
|
|
||||||
Silme yolunda ekranın veri aynası (`ListForm.SeedFilePath` → `data/{ListFormCode}.json` ya da
|
Silme yolunda ekranın veri aynası (`ListForm.SeedFilePath` → `data/{ListFormCode}.json` ya da
|
||||||
klasörü) ekranla birlikte silinir; ekransız uygulanamaz.
|
klasörü) ekranla birlikte silinir; ekransız uygulanamaz.
|
||||||
|
|
@ -1509,7 +1563,7 @@ diyaloğunda tabloyu seçerse (`WizardDeleteInput.DropTables`) kayıt temizliği
|
||||||
(`{sql|postgres}/object/{Tablo}.sql`, `crud/{Tablo}.json`) silinir. Düşürme hatası (ör. yabancı
|
(`{sql|postgres}/object/{Tablo}.sql`, `crud/{Tablo}.json`) silinir. Düşürme hatası (ör. yabancı
|
||||||
anahtar) silmeyi durdurmaz: tablo ve scripti yerinde kalır, rapor `Failed` + mesaj gösterir.
|
anahtar) silmeyi durdurmaz: tablo ve scripti yerinde kalır, rapor `Failed` + mesaj gösterir.
|
||||||
Tablo düşürmek `App.SqlQueryManager` yetkisi ister; wizard silme yetkisi tek başına yetmez.
|
Tablo düşürmek `App.SqlQueryManager` yetkisi ister; wizard silme yetkisi tek başına yetmez.
|
||||||
Düzenleme (`Redeploy`) yolu tabloya hiç dokunmaz.
|
Düzenleme tabloya hiç dokunmaz.
|
||||||
|
|
||||||
### 4.6 Alt formlar ve widget'lar
|
### 4.6 Alt formlar ve widget'lar
|
||||||
|
|
||||||
|
|
@ -2803,11 +2857,12 @@ düşerdi; **(b)** hedefte eksik kolonlar eklenir; **(c)** `crud/*.json` → `IC
|
||||||
**(d)** `data/*.json` →
|
**(d)** `data/*.json` →
|
||||||
`ListFormSeedDataApplier` (tabloda olmayan satır eklenir, var olan satır dosyadaki değerlerle
|
`ListFormSeedDataApplier` (tabloda olmayan satır eklenir, var olan satır dosyadaki değerlerle
|
||||||
güncellenir, soft delete edilmiş satırın `IsDeleted`/`DeletionTime`/`DeleterId` izleri temizlenir).
|
güncellenir, soft delete edilmiş satırın `IsDeleted`/`DeletionTime`/`DeleterId` izleri temizlenir).
|
||||||
**(e)** `wizard/*.json` → ekran kurulur. Kurulum wizard ekranıyla aynı yoldan geçer
|
Wizard'ın menü/yetki/dil satırlarını taşıyan platform dosyaları (`WizardSeedRowStore.ListFormCodes`)
|
||||||
(`ListFormWizardAppService.DeployWizardAsync`); dosyadaki `ListForm`/`Fields` sözleşmesi olduğu gibi
|
bu adımda **uygulanmaz**. **(e)** `wizard/*.json` → ekran **kurulmaz**: dosyaya `Pending` yazılır,
|
||||||
kullanılır, wizard cevaplarından yeniden türetilmez (türetmek tasarımcı değişikliklerini silerdi).
|
Wizard Manager onu "Migrate bekliyor" gösterir ve ekran, menü ve yetkiler bir sonraki DB Migrate'te
|
||||||
Hedefte zaten var olan ekrana dokunulmaz ve her ekran kendi UoW'unda kurulur. Bu adım rollback
|
gelir (§4.0) — hedefte kurulu olan ekran da dosyadaki hâliyle yeniden kurulur. Bu adım rollback
|
||||||
kapsamında değildir. Migrate beklenmez; import bittiğinde ekran çalışır durumdadır.
|
kapsamında değildir. Export zip'i wizard'ın platform satırlarını (`WizardSeedRowKeys.All`) ilgili
|
||||||
|
dosyalardan süzerek taşır; import onları hedef dosyayla anahtar üzerinden birleştirir.
|
||||||
|
|
||||||
**Analiz doğrulama da yapar** (`WizardImportValidator`) — dosyalar yazılmadan önce. En az bir
|
**Analiz doğrulama da yapar** (`WizardImportValidator`) — dosyalar yazılmadan önce. En az bir
|
||||||
`Error` varken import başlatılamaz; sunucu oturuma `blocked` işaret dosyası bırakır, dolayısıyla
|
`Error` varken import başlatılamaz; sunucu oturuma `blocked` işaret dosyası bırakır, dolayısıyla
|
||||||
|
|
@ -4117,7 +4172,7 @@ Prompt ile üretim yaptıysan ek olarak:
|
||||||
- [ ] Custom component visual modda ise `Props.visualDesigner` ile `Code` marker'ı aynı dokümanı
|
- [ ] Custom component visual modda ise `Props.visualDesigner` ile `Code` marker'ı aynı dokümanı
|
||||||
taşıyor.
|
taşıyor.
|
||||||
- [ ] Menü ve route elle eklenmedi; wizard dosyasından geliyor.
|
- [ ] Menü ve route elle eklenmedi; wizard dosyasından geliyor.
|
||||||
- [ ] List ekranı deploy ile kuruldu (wizard ekranı / MCP `deploy_wizard`) ya da elle yazılan
|
- [ ] List ekranı deploy ile üretildi (wizard ekranı / MCP `deploy_wizard`) ya da elle yazılan
|
||||||
dosya `ListForm` + `Fields` bölümlerini taşıyor (§4.1).
|
dosya `ListForm` + `Fields` bölümlerini taşıyor (§4.1).
|
||||||
- [ ] Kullanıcıya hangi dosyaların üretildiği ve DB Migrate gerektiği söylendi.
|
- [ ] Kullanıcıya hangi dosyaların üretildiği ve DB Migrate gerektiği söylendi.
|
||||||
- [ ] Keşif turu işletildi (§0.6): Tur 1 soruları soruldu, yol için gerekçe verildi ve tarif
|
- [ ] Keşif turu işletildi (§0.6): Tur 1 soruları soruldu, yol için gerekçe verildi ve tarif
|
||||||
|
|
|
||||||
29
README.md
29
README.md
|
|
@ -506,14 +506,20 @@ adım sayısından bağımsızdır.
|
||||||
| İş akışı | `WizardStepWorkflow.tsx` | Onay/koşul grafiği |
|
| İş akışı | `WizardStepWorkflow.tsx` | Onay/koşul grafiği |
|
||||||
| Düzen adımları | `WizardStepTodoLayout` · `WizardStepTreeLayout` · `WizardStepGanttLayout` · `WizardStepSchedulerLayout` | Yalnızca ilgili görünüm açıksa görünür |
|
| Düzen adımları | `WizardStepTodoLayout` · `WizardStepTreeLayout` · `WizardStepGanttLayout` · `WizardStepSchedulerLayout` | Yalnızca ilgili görünüm açıksa görünür |
|
||||||
| Bileşen | `WizardStepComponent.tsx` · `WizardStepCustomComponent.tsx` | Custom yolunda bağlanacak bileşen |
|
| Bileşen | `WizardStepComponent.tsx` · `WizardStepCustomComponent.tsx` | Custom yolunda bağlanacak bileşen |
|
||||||
| Yayınlama | `WizardStepDeploy.tsx` | Özet, doğrulama ve seed dosyasının üretimi |
|
| Yayınlama | `WizardStepDeploy.tsx` | Özet, doğrulama ve seed dosyalarının üretimi; ardından Wizard Manager'a dönülü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
|
**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
|
`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
|
(`Fields`) tutar; `WizardDataSeeder` ekranı bu iki bölümden kurar. **Deploy veritabanına yazmaz:**
|
||||||
|
wizard dosyasının yanında menü, modül, izin grubu, yetki ve dil metinleri platform seed
|
||||||
|
dosyalarına (`data/App.Menus.{Menu,Module}.json`, `data/Abp.Identity.Permission{Groups,s}.json`,
|
||||||
|
`data/App.Languages.LanguageText.json`) satır olarak yazılır (`WizardSeedRowStore`) ve hepsi DB
|
||||||
|
Migrate ile kurulur. Deploy/import dosyaya `Pending` yazar; Wizard Manager dosyayı "Migrate
|
||||||
|
bekliyor" rozetiyle gösterir, migrate bitince liste ve menü yenilenir. Yetkiler hiçbir role
|
||||||
|
otomatik atanmaz. 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.
|
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
|
`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
|
yolu, alan başlıklarının dil karşılıkları) taşır; `ListForm`/`Fields` içinde karşılığı olan alanlar
|
||||||
|
|
@ -652,13 +658,12 @@ Adımların altında yatan üç sunucu işlemi şunlardır:
|
||||||
sayısı dialogda raporlanır. Bu adım `RollbackImport`
|
sayısı dialogda raporlanır. Bu adım `RollbackImport`
|
||||||
kapsamında **değildir** — geri alma bu adımdan önce yapılır.
|
kapsamında **değildir** — geri alma bu adımdan önce yapılır.
|
||||||
|
|
||||||
5. **Ekran tanımları** (`wizard/*.json`) — son adım: ekranın tablosu, CRUD ucu ve lookup verileri o
|
5. **Ekran tanımları** (`wizard/*.json`) — ekran import'ta **kurulmaz**: dosyaya `Pending` yazılır,
|
||||||
noktada hazırdır. Kurulum wizard ekranının kullandığı yolun aynısından geçer
|
Wizard Manager onu "Migrate bekliyor" gösterir ve ekran, deploy'daki gibi, DB Migrate'te
|
||||||
(`DeployWizardAsync`), dosya ekranın üretilmiş `ListForm`/`Fields` sözleşmesini taşıdığı için
|
`WizardDataSeeder` tarafından kurulur (hedefte kurulu olsa da dosyadaki hâliyle). Dosya ekranın
|
||||||
bunlar wizard cevaplarından **yeniden türetilmez** — tasarımcı üzerindeki her değişiklik olduğu
|
üretilmiş `ListForm`/`Fields` sözleşmesini taşıdığı için bunlar wizard cevaplarından **yeniden
|
||||||
gibi kurulur. **Hedefte zaten var olan ekrana dokunulmaz.** Her ekran kendi işleminde kurulur:
|
türetilmez**. Wizard'ın menü/yetki/dil satırlarını taşıyan platform dosyaları 4. adımda
|
||||||
bir dosyadaki hata yalnızca o ekranın kayıtlarını geri alır. İmport bittiğinde ekran çalışır
|
uygulanmaz; onları da migrate ekler.
|
||||||
durumdadır, migrate beklemez; istemci ABP config'ini yeniler, böylece menü anında görünür.
|
|
||||||
|
|
||||||
**Analiz doğrulama da yapar** (`WizardImportValidator`). Zip'in hedef ortamda yarım bir kurulum
|
**Analiz doğrulama da yapar** (`WizardImportValidator`). Zip'in hedef ortamda yarım bir kurulum
|
||||||
bırakıp bırakmayacağı dosyalar yazılmadan **önce** denetlenir; bulgular `Error`/`Warning` olarak
|
bırakıp bırakmayacağı dosyalar yazılmadan **önce** denetlenir; bulgular `Error`/`Warning` olarak
|
||||||
|
|
@ -911,8 +916,8 @@ Sıra **kardeşler arasındadır**: her üst yetkinin altında 1'den başlar, gr
|
||||||
kendi aralarında 1'den. `PlatformPermissionAppService` sıralama anahtarını kökten yaprağa üst yetki
|
kendi aralarında 1'den. `PlatformPermissionAppService` sıralama anahtarını kökten yaprağa üst yetki
|
||||||
zincirinden kurar, böylece çocuk her zaman ebeveyninin altında kalır. Değerler seed dosyalarındaki
|
zincirinden kurar, böylece çocuk her zaman ebeveyninin altında kalır. Değerler seed dosyalarındaki
|
||||||
`Order` alanında tutulur ve Permissions / Permission Groups ekranlarından düzenlenebilir.
|
`Order` alanında tutulur ve Permissions / Permission Groups ekranlarından düzenlenebilir.
|
||||||
Wizard (ve migrate'te `WizardDataSeeder`) yeni grubu ve grubun yeni kök yetkisini kardeşlerin en
|
Wizard deploy'u yeni grubu ve grubun yeni kök yetkisini (seed satırı olarak) veritabanı ile seed
|
||||||
büyük sırasının bir fazlasıyla, alt yetkileri Create/Update/Delete/Export/Import/Note dizilimiyle
|
dosyasındaki kardeşlerin en büyük sırasının bir fazlasıyla, alt yetkileri Create/Update/Delete/Export/Import/Note dizilimiyle
|
||||||
1'den yazar (`NextPermissionOrder`, `AbpPermissionsExtensions`).
|
1'den yazar (`NextPermissionOrder`, `AbpPermissionsExtensions`).
|
||||||
|
|
||||||
Aksiyon yetkisi olan alt gruplar (UI tarafındaki karşılıkları `constants/permission.constant.ts`
|
Aksiyon yetkisi olan alt gruplar (UI tarafındaki karşılıkları `constants/permission.constant.ts`
|
||||||
|
|
@ -1401,7 +1406,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
|
||||||
| `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`). Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. |
|
| `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`). Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. |
|
||||||
| `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `{kapsam}/sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. |
|
| `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `{kapsam}/sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. |
|
||||||
| `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. |
|
| `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. |
|
||||||
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. `replaceFileName` doluysa güncellemedir (eski dosya ve ürettiği kayıtlar silinip yeniden üretilir). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — ardından satırlar `insert_row` ile eklenir; `ListFormSeedDataSynchronizer` onları `configs/seeds/host/data/` altına yazar, migrate ekranla birlikte geri yükler (`lowcode.instructions.md` §0.10.1). |
|
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (`verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — migrate sonrasında satırlar `insert_row` ile eklenir; `ListFormSeedDataSynchronizer` onları `configs/seeds/host/data/` altına yazar, migrate ekranla birlikte geri yükler (`lowcode.instructions.md` §0.10.1). |
|
||||||
| `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). |
|
| `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). |
|
||||||
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). `App.DevRequests` geliştirme döngüsünün test adımıdır (`lowcode.instructions.md` §0.11). |
|
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). `App.DevRequests` geliştirme döngüsünün test adımıdır (`lowcode.instructions.md` §0.11). |
|
||||||
| `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. `.Write` + `App.DevRequests.Update` ister. |
|
| `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. `.Write` + `App.DevRequests.Update` ister. |
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -57,6 +57,7 @@ public class McpDevelopAppService(
|
||||||
private static class VerifyChecks
|
private static class VerifyChecks
|
||||||
{
|
{
|
||||||
public const string WizardFile = "wizard_file";
|
public const string WizardFile = "wizard_file";
|
||||||
|
public const string Install = "install";
|
||||||
public const string Menu = "menu";
|
public const string Menu = "menu";
|
||||||
public const string Permission = "permission";
|
public const string Permission = "permission";
|
||||||
public const string Select = "select";
|
public const string Select = "select";
|
||||||
|
|
@ -219,6 +220,16 @@ public class McpDevelopAppService(
|
||||||
result.MenuUrl = wizard.MenuUrl;
|
result.MenuUrl = wizard.MenuUrl;
|
||||||
result.Checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.WizardFile, Passed = true });
|
result.Checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.WizardFile, Passed = true });
|
||||||
|
|
||||||
|
// Deploy yalnizca seed dosyalarini yazar; menu, yetki ve ekran DB Migrate'ten sonra vardir.
|
||||||
|
// Kurulmamis bir wizard'in geri kalan kontrolleri anlamsiz hatalar uretirdi.
|
||||||
|
var file = (await wizardAppService.GetFiles())
|
||||||
|
.FirstOrDefault(f => string.Equals(f.FileName, fileName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (file?.IsPending == true)
|
||||||
|
{
|
||||||
|
result.Checks.Add(Failed(VerifyChecks.Install, L[McpErrorCodes.VerifyWizardPending, fileName]));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
var permissionName = listFormCode;
|
var permissionName = listFormCode;
|
||||||
if (wizard.CreateMenu)
|
if (wizard.CreateMenu)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ public static class McpErrorCodes
|
||||||
public const string ComponentCodeRequired = Prefix + "ComponentCodeRequired";
|
public const string ComponentCodeRequired = Prefix + "ComponentCodeRequired";
|
||||||
public const string ComponentNotFound = Prefix + "ComponentNotFound";
|
public const string ComponentNotFound = Prefix + "ComponentNotFound";
|
||||||
public const string VerifyWizardFileInvalid = Prefix + "VerifyWizardFileInvalid";
|
public const string VerifyWizardFileInvalid = Prefix + "VerifyWizardFileInvalid";
|
||||||
|
public const string VerifyWizardPending = Prefix + "VerifyWizardPending";
|
||||||
public const string VerifyMenuMissing = Prefix + "VerifyMenuMissing";
|
public const string VerifyMenuMissing = Prefix + "VerifyMenuMissing";
|
||||||
public const string VerifyMenuDisabled = Prefix + "VerifyMenuDisabled";
|
public const string VerifyMenuDisabled = Prefix + "VerifyMenuDisabled";
|
||||||
public const string VerifyMenuUrlMismatch = Prefix + "VerifyMenuUrlMismatch";
|
public const string VerifyMenuUrlMismatch = Prefix + "VerifyMenuUrlMismatch";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
@ -64,7 +64,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
|
||||||
=> await developAppService.GetWizardAsync(fileName);
|
=> await developAppService.GetWizardAsync(fileName);
|
||||||
|
|
||||||
[McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)]
|
[McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)]
|
||||||
[Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari, menusu ve seed dosyasi birlikte uretilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve deploy'dan sonra her satiri insert_row ile ekle: satirlar seed klasorundeki data/{ListFormCode}.json dosyasina yazilir ve migrate ile ekranla birlikte geri yuklenir; SeedData olmadan eklenen satir yalnizca bu veritabaninda kalir. replaceFileName verilirse o wizard ve urettigi kayitlar silinip yeniden uretilir; ekran wizard cevaplarindan yeniden turetildigi icin tasarimcida yapilmis ve wizard'da karsiligi olmayan ayarlar kaybolur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")]
|
[Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme.Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz; DB Migrate calistiktan sonra (isPending false, verify_wizard gecer) her satiri insert_row ile ekle: satirlar seed klasorundeki data/{ListFormCode}.json dosyasina yazilir ve migrate ile ekranla birlikte geri yuklenir; SeedData olmadan eklenen satir yalnizca bu veritabaninda kalir. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir; ekran wizard cevaplarindan yeniden turetildigi icin tasarimcida yapilmis ve wizard'da karsiligi olmayan ayarlar kaybolur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")]
|
||||||
public async Task<WizardFileInfoDto?> DeployWizardAsync(
|
public async Task<WizardFileInfoDto?> DeployWizardAsync(
|
||||||
[Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}}, get_wizard cikisi gibi) ya da duz wizard cevaplari. Enum alanlari sayi olarak yazilir.")]
|
[Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}}, get_wizard cikisi gibi) ya da duz wizard cevaplari. Enum alanlari sayi olarak yazilir.")]
|
||||||
string wizard,
|
string wizard,
|
||||||
|
|
@ -82,7 +82,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
|
||||||
=> await developAppService.DeleteWizardAsync(fileName, dropTables);
|
=> await developAppService.DeleteWizardAsync(fileName, dropTables);
|
||||||
|
|
||||||
[McpServerTool(Name = McpToolNames.VerifyWizard, ReadOnly = true)]
|
[McpServerTool(Name = McpToolNames.VerifyWizard, ReadOnly = true)]
|
||||||
[Description("Deploy edilmis bir wizard'i veri yazmadan dogrular: menu kaydi (var mi, etkin mi, URL'si wizard'la ayni mi), menunun istedigi yetki tanimi, liste ekraninda ekranin sorgusu (cagiranin yetkisiyle tek satir), Custom yolunda bilesen (var mi, etkin mi, rotasi menuyle ayni mi) ve seed kapanisi (migrate ya da baska ortamda eksik kalacak bagimlilik). passed yalnizca blocking kontrollerden biri kalirsa false olur; seed_closure engellemez ama raporlanmalidir. deploy_wizard'dan sonra ve bir gelistirme talebini Staging durumuna almadan once cagir.")]
|
[Description("Deploy edilip DB Migrate ile kurulmus bir wizard'i veri yazmadan dogrular (migrate bekleyen wizard'da install kontrolu basarisiz doner ve diger kontroller yapilmaz): menu kaydi (var mi, etkin mi, URL'si wizard'la ayni mi), menunun istedigi yetki tanimi, liste ekraninda ekranin sorgusu (cagiranin yetkisiyle tek satir), Custom yolunda bilesen (var mi, etkin mi, rotasi menuyle ayni mi) ve seed kapanisi (migrate ya da baska ortamda eksik kalacak bagimlilik). passed yalnizca blocking kontrollerden biri kalirsa false olur; seed_closure engellemez ama raporlanmalidir. deploy_wizard'dan sonra ve bir gelistirme talebini Staging durumuna almadan once cagir.")]
|
||||||
public async Task<McpVerifyResultDto> VerifyWizardAsync(
|
public async Task<McpVerifyResultDto> VerifyWizardAsync(
|
||||||
[Description("Dogrulanacak wizard dosyasinin adi (list_wizards ya da deploy_wizard cikisindaki fileName).")]
|
[Description("Dogrulanacak wizard dosyasinin adi (list_wizards ya da deploy_wizard cikisindaki fileName).")]
|
||||||
string fileName)
|
string fileName)
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,18 @@ public class WizardSeedFileDto
|
||||||
public bool IsCreatedField { get; set; }
|
public bool IsCreatedField { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bu Wizard çalışırken gerçekten YENİ oluşturulan kayıtlar.
|
/// Wizard'in sahip oldugu kayitlar: deploy sirasinda ne veritabaninda ne seed dosyalarinda
|
||||||
/// Daha önce var olan kayıtlar buraya eklenmez, silme işleminde sadece bu liste kullanılır.
|
/// bulunan, satiri ilk kez bu wizard'in yazdigi kayitlar. Paylasilan kayitlar buraya girmez;
|
||||||
|
/// silme ve duzenleme yalnizca bu listeye dokunur.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public WizardInsertedRecordsDto InsertedRecords { get; set; } = new();
|
public WizardInsertedRecordsDto InsertedRecords { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Doluysa dosya henuz veritabanina uygulanmamistir: deploy yalnizca dosyalari yazar, kurulumu
|
||||||
|
/// bir sonraki migrate yapar (<c>WizardDataSeeder</c>) ve bu bolumu temizler.
|
||||||
|
/// </summary>
|
||||||
|
public WizardPendingDto Pending { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dosyaya yazmadan once ortama ozgu alanlari temizler. Kayit kimlikleri ve audit damgalari
|
/// Dosyaya yazmadan once ortama ozgu alanlari temizler. Kayit kimlikleri ve audit damgalari
|
||||||
/// veritabanina aittir; dosyada tasinirlarsa her ortamda anlamsiz fark uretirler. EditType ise
|
/// veritabanina aittir; dosyada tasinirlarsa her ortamda anlamsiz fark uretirler. EditType ise
|
||||||
|
|
@ -161,7 +168,13 @@ public class WizardSeedFileDto
|
||||||
private const string MenuSection = "Menu";
|
private const string MenuSection = "Menu";
|
||||||
private const string LanguageSection = "Language";
|
private const string LanguageSection = "Language";
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions SeedWriteOptions = new() { WriteIndented = true };
|
// Varsayilan kodlayici Turkce harfleri de kacirir ("Kullanıcı"); dosya elle okunur ve
|
||||||
|
// diff'lenir, bu yuzden veri dosyalariyla ayni kodlayici kullanilir.
|
||||||
|
private static readonly JsonSerializerOptions SeedWriteOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
|
};
|
||||||
private static readonly JsonSerializerOptions SeedReadOptions = new() { PropertyNameCaseInsensitive = true };
|
private static readonly JsonSerializerOptions SeedReadOptions = new() { PropertyNameCaseInsensitive = true };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -202,6 +215,11 @@ public class WizardSeedFileDto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bekleyen kurulum yoksa bolum dosyada hic yer almaz; kurulu bir wizard'in dosyasi
|
||||||
|
// migrate sonrasinda deploy oncesiyle birebir ayni kalir.
|
||||||
|
if (Pending == null && node is JsonObject root)
|
||||||
|
root.Remove(nameof(Pending));
|
||||||
|
|
||||||
if (node?[nameof(Wizard)] is JsonObject wizardNode)
|
if (node?[nameof(Wizard)] is JsonObject wizardNode)
|
||||||
{
|
{
|
||||||
// Baglanti cumlesi yalnizca yeni bir veri kaynagi tanimlanirken doludur; bos degeri
|
// Baglanti cumlesi yalnizca yeni bir veri kaynagi tanimlanirken doludur; bos degeri
|
||||||
|
|
@ -230,7 +248,7 @@ public class WizardSeedFileDto
|
||||||
node[nameof(Wizard)] = Reorder(wizardNode, WizardKeyOrder);
|
node[nameof(Wizard)] = Reorder(wizardNode, WizardKeyOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
return node?.ToJsonString(SeedWriteOptions) ?? string.Empty;
|
return SeedJsonText.RestoreSurrogatePairs(node?.ToJsonString(SeedWriteOptions) ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -365,12 +383,51 @@ public class WizardInsertedRecordsDto
|
||||||
/// <summary>Bu Wizard tarafından oluşturulan yeni Module kayıtları.</summary>
|
/// <summary>Bu Wizard tarafından oluşturulan yeni Module kayıtları.</summary>
|
||||||
public List<string> ModuleIds { get; set; } = [];
|
public List<string> ModuleIds { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Iki listenin birlesimi; ayni kayit bir kez yer alir.</summary>
|
||||||
|
public WizardInsertedRecordsDto Union(WizardInsertedRecordsDto other)
|
||||||
|
{
|
||||||
|
static List<string> Merge(List<string> a, List<string> b) =>
|
||||||
|
[.. (a ?? []).Concat(b ?? []).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase)];
|
||||||
|
|
||||||
|
return new WizardInsertedRecordsDto
|
||||||
|
{
|
||||||
|
LanguageKeys = Merge(LanguageKeys, other?.LanguageKeys),
|
||||||
|
PermissionGroupNames = Merge(PermissionGroupNames, other?.PermissionGroupNames),
|
||||||
|
PermissionNames = Merge(PermissionNames, other?.PermissionNames),
|
||||||
|
MenuCodes = Merge(MenuCodes, other?.MenuCodes),
|
||||||
|
DataSourceCodes = Merge(DataSourceCodes, other?.DataSourceCodes),
|
||||||
|
ModuleIds = Merge(ModuleIds, other?.ModuleIds),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool HasAny =>
|
public bool HasAny =>
|
||||||
LanguageKeys.Count > 0 || PermissionGroupNames.Count > 0 || PermissionNames.Count > 0 ||
|
LanguageKeys.Count > 0 || PermissionGroupNames.Count > 0 || PermissionNames.Count > 0 ||
|
||||||
MenuCodes.Count > 0 || DataSourceCodes.Count > 0 || ModuleIds.Count > 0;
|
MenuCodes.Count > 0 || DataSourceCodes.Count > 0 || ModuleIds.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deploy edilmis ama henuz migrate edilmemis bir wizard'in bekleyen kurulumu.
|
||||||
|
/// <para>
|
||||||
|
/// Duzenleme veritabanina dokunmadigi icin onceki surumun kurdugu kayitlar migrate'e kadar yerinde
|
||||||
|
/// durur. <see cref="Replaced"/> onceki surumlerin sahip oldugu kayitlardir; migrate bunlari (ve
|
||||||
|
/// wizard'in guncel kayitlarini) veritabanindan kaldirir, ardindan seed dosyalarindaki guncel
|
||||||
|
/// satirlar eklenir. Boylece ad ya da kod degistiren bir duzenlemede eski kayit artik kalmaz,
|
||||||
|
/// metni degisen kayit yeni metniyle gelir.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public class WizardPendingDto
|
||||||
|
{
|
||||||
|
/// <summary>Ilk bekleyen deploy zamani; migrate edilmeden yapilan duzenlemeler degistirmez.</summary>
|
||||||
|
public DateTime Since { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Onceki surumlerin sahip oldugu kayitlar.</summary>
|
||||||
|
public WizardInsertedRecordsDto Replaced { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Onceki surumlerin kurdugu ekran kodlari; kod degistiyse eski ekran kaldirilir.</summary>
|
||||||
|
public List<string> ListFormCodes { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Kaydi kullanan (silinmesine engel olan) taraf.</summary>
|
/// <summary>Kaydi kullanan (silinmesine engel olan) taraf.</summary>
|
||||||
public class WizardRecordRefDto
|
public class WizardRecordRefDto
|
||||||
{
|
{
|
||||||
|
|
@ -456,6 +513,12 @@ public class WizardFileInfoDto
|
||||||
public string CreatedAt { get; set; }
|
public string CreatedAt { get; set; }
|
||||||
public bool HasInsertedRecords { get; set; }
|
public bool HasInsertedRecords { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dosya veritabanina henuz uygulanmadi: deploy edildi ya da duzenlendi ama migrate calismadi,
|
||||||
|
/// veya ekran bu veritabaninda hic kurulmadi. Kurulum DB Migrate ile yapilir.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPending { get; set; }
|
||||||
|
|
||||||
/// <summary>Wizard'in List mi Custom Component mi urettigi; listede rozet olarak gosterilir.</summary>
|
/// <summary>Wizard'in List mi Custom Component mi urettigi; listede rozet olarak gosterilir.</summary>
|
||||||
public WizardComponentKindEnum ComponentKind { get; set; }
|
public WizardComponentKindEnum ComponentKind { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.ListForms;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir wizard'in platform seed dosyalarindaki (menu, modul, izin grubu, yetki, dil metni)
|
||||||
|
/// satirlarinin anahtarlari. Anahtarlar yalnizca wizard cevaplarindan turer; deploy satirlari bu
|
||||||
|
/// anahtarlarla yazar, migrate admin rolunun yetkilerini ve export arsivin satirlarini ayni listeden
|
||||||
|
/// bulur. Kuralin ikinci bir kopyasi yazilmaz.
|
||||||
|
/// </summary>
|
||||||
|
public static class WizardSeedRowKeys
|
||||||
|
{
|
||||||
|
/// <summary>Okuma yetkisinin altindaki alt yetkiler: (sonek, dil anahtari), siralari dizilimdir.</summary>
|
||||||
|
public static (string Suffix, string LangKey)[] ChildPermissions(ListFormWizardDto wizard)
|
||||||
|
{
|
||||||
|
(string, string)[] crud =
|
||||||
|
[
|
||||||
|
("Create", WizardConsts.LangKeyCreate),
|
||||||
|
("Update", WizardConsts.LangKeyUpdate),
|
||||||
|
("Delete", WizardConsts.LangKeyDelete),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Export/Import/Note yalnizca ListForm ekosisteminin sundugu islemlerdir.
|
||||||
|
return wizard.ComponentKind == WizardComponentKindEnum.Custom
|
||||||
|
? crud
|
||||||
|
: [.. crud, ("Export", WizardConsts.LangKeyExport), ("Import", WizardConsts.LangKeyImport), ("Note", WizardConsts.LangKeyNote)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Menuye eklenmeyen wizard'da izin grubu sorulmaz; bos geldiginde uygulama grubuna duser.</summary>
|
||||||
|
public static string PermissionGroupName(ListFormWizardDto wizard) =>
|
||||||
|
string.IsNullOrWhiteSpace(wizard.PermissionGroupName) ? PlatformConsts.AppName : wizard.PermissionGroupName.Trim();
|
||||||
|
|
||||||
|
/// <summary>Okuma yetkisi ve alt yetkileri; ilk oge okuma yetkisidir.</summary>
|
||||||
|
public static List<string> PermissionNames(ListFormWizardDto wizard)
|
||||||
|
{
|
||||||
|
var code = wizard.ResolveMenuCode();
|
||||||
|
return [code, .. ChildPermissions(wizard).Select(p => $"{code}.{p.Suffix}")];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wizard'in dokundugu butun satirlarin anahtarlari: kendi kayitlari ve baglandigi kaplar (ust
|
||||||
|
/// menu, modul, izin grubu). Kaplar paylasilan kayitlardir; baska bir ortama tasinirken hedefte
|
||||||
|
/// olmayabilecekleri icin export'a girerler, ama sahipligi <c>InsertedRecords</c> belirler.
|
||||||
|
/// </summary>
|
||||||
|
public static WizardInsertedRecordsDto All(ListFormWizardDto wizard)
|
||||||
|
{
|
||||||
|
var code = wizard.ResolveMenuCode();
|
||||||
|
var group = PermissionGroupName(wizard);
|
||||||
|
var isList = wizard.ComponentKind != WizardComponentKindEnum.Custom;
|
||||||
|
|
||||||
|
var menus = new List<string> { code };
|
||||||
|
var languageKeys = new List<string> { code, group };
|
||||||
|
var modules = new List<string>();
|
||||||
|
|
||||||
|
if (wizard.CreateMenu && !string.IsNullOrWhiteSpace(wizard.MenuParentCode))
|
||||||
|
{
|
||||||
|
menus.Add(wizard.MenuParentCode.Trim());
|
||||||
|
languageKeys.Add(wizard.MenuParentCode.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(wizard.MenuParentModuleId))
|
||||||
|
modules.Add(wizard.MenuParentModuleId.Trim());
|
||||||
|
|
||||||
|
if (isList)
|
||||||
|
{
|
||||||
|
var listFormCode = wizard.ResolveListFormCode();
|
||||||
|
languageKeys.Add($"{listFormCode}.Title");
|
||||||
|
languageKeys.Add($"{listFormCode}.Desc");
|
||||||
|
languageKeys.AddRange((wizard.Groups ?? [])
|
||||||
|
.SelectMany(g => g.Items ?? [])
|
||||||
|
.Select(i => i.CaptionName)
|
||||||
|
.Where(k => !string.IsNullOrWhiteSpace(k)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WizardInsertedRecordsDto
|
||||||
|
{
|
||||||
|
MenuCodes = wizard.CreateMenu ? menus : [],
|
||||||
|
ModuleIds = wizard.CreateMenu ? modules : [],
|
||||||
|
PermissionGroupNames = [group],
|
||||||
|
PermissionNames = PermissionNames(wizard),
|
||||||
|
LanguageKeys = [.. languageKeys.Distinct(StringComparer.OrdinalIgnoreCase)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -457,6 +457,6 @@ public class ListFormSeedDataSynchronizer(
|
||||||
private static async Task WriteSeedFileAsync(string filePath, ListFormSeedDataFileDto seedFile)
|
private static async Task WriteSeedFileAsync(string filePath, ListFormSeedDataFileDto seedFile)
|
||||||
{
|
{
|
||||||
seedFile.GeneratedAt = DateTime.UtcNow;
|
seedFile.GeneratedAt = DateTime.UtcNow;
|
||||||
await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(seedFile, ListFormSeedDataFileDto.JsonOptions));
|
await File.WriteAllTextAsync(filePath, ListFormSeedDataFileDto.Serialize(seedFile));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -54,6 +54,9 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
private readonly HashSet<string> customComponents = new(StringComparer.OrdinalIgnoreCase);
|
private readonly HashSet<string> customComponents = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly List<string> warnings = [];
|
private readonly List<string> warnings = [];
|
||||||
|
|
||||||
|
/// <summary>Toplanan wizard'larin platform seed dosyalarindaki (menu, yetki, dil) satir anahtarlari.</summary>
|
||||||
|
private WizardInsertedRecordsDto platformRows = new();
|
||||||
|
|
||||||
/// <summary>ListFormCode -> wizard seed dosyasi; alt form cozumu icin bir kez kurulur.</summary>
|
/// <summary>ListFormCode -> wizard seed dosyasi; alt form cozumu icin bir kez kurulur.</summary>
|
||||||
private Dictionary<string, string>? wizardFilesByListFormCode;
|
private Dictionary<string, string>? wizardFilesByListFormCode;
|
||||||
|
|
||||||
|
|
@ -84,6 +87,7 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
AddDiskFile($"{SeedPathResolver.WizardFolder}/{System.IO.Path.GetFileName(filePath)}", reason);
|
AddDiskFile($"{SeedPathResolver.WizardFolder}/{System.IO.Path.GetFileName(filePath)}", reason);
|
||||||
|
platformRows = platformRows.Union(WizardSeedRowKeys.All(seed.Wizard));
|
||||||
|
|
||||||
if (seed.Wizard.ComponentKind == WizardComponentKindEnum.Custom)
|
if (seed.Wizard.ComponentKind == WizardComponentKindEnum.Custom)
|
||||||
await CollectCustomAsync(seed.Wizard.CustomComponentName, seed.Wizard.WizardName);
|
await CollectCustomAsync(seed.Wizard.CustomComponentName, seed.Wizard.WizardName);
|
||||||
|
|
@ -92,6 +96,7 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
await MaterializeCustomComponentsAsync();
|
await MaterializeCustomComponentsAsync();
|
||||||
|
await MaterializePlatformRowsAsync();
|
||||||
|
|
||||||
return [.. items.Values.OrderBy(x => x.Path, StringComparer.OrdinalIgnoreCase)];
|
return [.. items.Values.OrderBy(x => x.Path, StringComparer.OrdinalIgnoreCase)];
|
||||||
}
|
}
|
||||||
|
|
@ -272,12 +277,58 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
Rows = rows,
|
Rows = rows,
|
||||||
};
|
};
|
||||||
|
|
||||||
var content = JsonSerializer.Serialize(filtered, ListFormSeedDataFileDto.JsonOptions);
|
var content = ListFormSeedDataFileDto.Serialize(filtered);
|
||||||
|
|
||||||
items[relativePath] = new WizardExportItem(relativePath, "Custom component tanımı", null, content);
|
items[relativePath] = new WizardExportItem(relativePath, "Custom component tanımı", null, content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wizard'in menu, modul, izin grubu, yetki ve dil metni satirlari paylasilan platform seed
|
||||||
|
/// dosyalarindadir; ekran bu satirlar olmadan hedefte menusuz ve yetkisiz kurulur. Her dosya
|
||||||
|
/// yalnizca toplanan wizard'larin satirlarina indirgenerek arsive girer; import satirlari hedef
|
||||||
|
/// dosyayla anahtar uzerinden birlestirir, migrate eksik olanlari ekler.
|
||||||
|
/// </summary>
|
||||||
|
private async Task MaterializePlatformRowsAsync()
|
||||||
|
{
|
||||||
|
(string ListFormCode, string Field, List<string> Keys)[] targets =
|
||||||
|
[
|
||||||
|
(WizardSeedRowStore.ModuleFile, "Id", platformRows.ModuleIds),
|
||||||
|
(WizardSeedRowStore.MenuFile, "Code", platformRows.MenuCodes),
|
||||||
|
(WizardSeedRowStore.PermissionGroupFile, "Name", platformRows.PermissionGroupNames),
|
||||||
|
(WizardSeedRowStore.PermissionFile, "Name", platformRows.PermissionNames),
|
||||||
|
(WizardSeedRowStore.LanguageTextFile, "Key", platformRows.LanguageKeys),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (var (listFormCode, field, keys) in targets)
|
||||||
|
{
|
||||||
|
var relativePath = SeedPathResolver.GetDataFilePath(listFormCode);
|
||||||
|
var fullPath = ResolveScopeFilePath(relativePath);
|
||||||
|
if (fullPath == null || keys.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var seedFile = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(
|
||||||
|
await File.ReadAllTextAsync(fullPath), ListFormSeedDataFileDto.JsonOptions);
|
||||||
|
|
||||||
|
var wanted = keys.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var rows = seedFile?.Rows?.Where(x => wanted.Contains(GetRowText(x, field) ?? string.Empty)).ToList() ?? [];
|
||||||
|
if (seedFile == null || rows.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var filtered = new ListFormSeedDataFileDto
|
||||||
|
{
|
||||||
|
ListFormCode = seedFile.ListFormCode,
|
||||||
|
KeyFieldName = seedFile.KeyFieldName,
|
||||||
|
GeneratedAt = seedFile.GeneratedAt,
|
||||||
|
Order = seedFile.Order,
|
||||||
|
Rows = rows,
|
||||||
|
};
|
||||||
|
|
||||||
|
items[relativePath] = new WizardExportItem(
|
||||||
|
relativePath, "Menü, yetki ve dil satırları", null, ListFormSeedDataFileDto.Serialize(filtered));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Component adi -> dogrudan bagimliliklari. Wizard listesi gruplamayi export'un izledigi
|
/// Component adi -> dogrudan bagimliliklari. Wizard listesi gruplamayi export'un izledigi
|
||||||
/// zincirle ayni kaynaktan kurar; boylece listede birlikte gorunenler arsive de birlikte girer.
|
/// zincirle ayni kaynaktan kurar; boylece listede birlikte gorunenler arsive de birlikte girer.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -28,17 +28,16 @@ namespace Sozsoft.Platform.ListForms;
|
||||||
/// ekranin kullandigi kayit birakilir ve kullananlar rapora yazilir.
|
/// ekranin kullandigi kayit birakilir ve kullananlar rapora yazilir.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <see cref="WizardDeleteMode.Redeploy"/> duzenleme yoludur: kayitlar hemen ardindan ayni
|
/// Wizard'in <b>baglandigi</b> kaplar da (ust menu zinciri, modul, izin grubu) bosaldiysa silinir: kap
|
||||||
/// cevaplardan yeniden uretilir. Bu yolda paylasilan kaplar (ust menu, izin grubu, modul, veri
|
/// bir baska wizard'la olusmus ve o wizard silinirken bu wizard'in menusu yuzunden birakilmis
|
||||||
/// kaynagi) ve diger rollerin yetki atamalari korunur; silinip yeniden uretilseler menu sirasi ve
|
/// olabilir; sahibi kalmayan kap artik olarak kalirdi. Bu kaplar yalnizca silindiklerinde rapora
|
||||||
/// rol atamalari kaybolurdu.
|
/// girer.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Silme yolunda wizard'in <b>baglandigi</b> kaplar da (ust menu zinciri, modul, izin grubu) bosaldiysa
|
/// Yalnizca kurulu (migrate edilmis) wizard icin kullanilir; migrate bekleyen wizard'in kayitlari
|
||||||
/// silinir: kap bir baska wizard'la olusmus ve o wizard silinirken bu wizard'in menusu yuzunden
|
/// yalnizca seed dosyalarindadir. Veritabani temizligi (<see cref="CleanAsync"/>) ile seed satirlarinin
|
||||||
/// birakilmis olabilir; sahibi kalmayan kap yeniden deploy'da artik olarak kalirdi. Bu kaplar
|
/// cikarilmasi (<see cref="RemoveSeedRowsAsync"/>) ayri adimlardir: satirlar ancak veritabani islemi
|
||||||
/// yalnizca silindiklerinde rapora girer. Silinen menu, modul, yetki ve izin grubu satirlari ekranlarin
|
/// basariyla tamamlandiktan sonra cikarilir, aksi halde dosya ile veritabani ayrisirdi.
|
||||||
/// seed dosyalarindan da cikarilir; aksi halde migrate onlari geri getirirdi.
|
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WizardRecordCleaner(
|
public class WizardRecordCleaner(
|
||||||
|
|
@ -65,20 +64,24 @@ public class WizardRecordCleaner(
|
||||||
/// <summary>Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.</summary>
|
/// <summary>Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.</summary>
|
||||||
private const int MaxUsedBy = 20;
|
private const int MaxUsedBy = 20;
|
||||||
|
|
||||||
public async Task<List<WizardDeleteLogItemDto>> CleanAsync(WizardSeedFileDto seed, WizardDeleteMode mode)
|
public async Task<List<WizardDeleteLogItemDto>> CleanAsync(WizardSeedFileDto seed)
|
||||||
{
|
{
|
||||||
var report = new List<WizardDeleteLogItemDto>();
|
var report = new List<WizardDeleteLogItemDto>();
|
||||||
var wizard = seed.Wizard;
|
var wizard = seed.Wizard;
|
||||||
var ins = seed.InsertedRecords ?? new WizardInsertedRecordsDto();
|
var ins = (seed.InsertedRecords ?? new WizardInsertedRecordsDto()).Union(seed.Pending?.Replaced);
|
||||||
|
|
||||||
// Custom yolunda ListForm uretilmez; ayni kodu tasiyan bir List wizard'inin ekrani silinmesin.
|
// Custom yolunda ListForm uretilmez; ayni kodu tasiyan bir List wizard'inin ekrani silinmesin.
|
||||||
if (wizard?.ComponentKind != WizardComponentKindEnum.Custom && !string.IsNullOrWhiteSpace(wizard?.ListFormCode))
|
if (wizard?.ComponentKind != WizardComponentKindEnum.Custom && !string.IsNullOrWhiteSpace(wizard?.ListFormCode))
|
||||||
await CleanListFormAsync(wizard.ListFormCode, mode, report);
|
await CleanListFormAsync(wizard.ListFormCode, report);
|
||||||
|
|
||||||
var deletedMenus = await CleanMenusAsync(ins.MenuCodes, wizard?.MenuParentCode, mode, report);
|
// Migrate edilmeden kodu degistirilen ekranin onceki surumu hala kuruludur.
|
||||||
|
foreach (var listFormCode in seed.Pending?.ListFormCodes ?? [])
|
||||||
|
await CleanListFormAsync(listFormCode, report);
|
||||||
|
|
||||||
|
var deletedMenus = await CleanMenusAsync(ins.MenuCodes, report);
|
||||||
var adoptedLanguageKeys = new List<string>();
|
var adoptedLanguageKeys = new List<string>();
|
||||||
|
|
||||||
if (mode == WizardDeleteMode.Delete && wizard?.CreateMenu == true)
|
if (wizard?.CreateMenu == true)
|
||||||
{
|
{
|
||||||
var parents = deletedMenus.Select(m => m.ParentCode).Append(wizard.MenuParentCode);
|
var parents = deletedMenus.Select(m => m.ParentCode).Append(wizard.MenuParentCode);
|
||||||
var emptyParents = await CleanEmptyParentMenusAsync(parents, deletedMenus, report);
|
var emptyParents = await CleanEmptyParentMenusAsync(parents, deletedMenus, report);
|
||||||
|
|
@ -86,33 +89,28 @@ public class WizardRecordCleaner(
|
||||||
adoptedLanguageKeys.AddRange(emptyParents.Select(m => m.DisplayName));
|
adoptedLanguageKeys.AddRange(emptyParents.Select(m => m.DisplayName));
|
||||||
}
|
}
|
||||||
|
|
||||||
await CleanPermissionsAsync(ins.PermissionNames, mode, report);
|
await CleanPermissionsAsync(ins.PermissionNames, report);
|
||||||
|
|
||||||
await CleanPermissionGroupsAsync(ins.PermissionGroupNames, mode, report, reportKept: true);
|
await CleanPermissionGroupsAsync(ins.PermissionGroupNames, report, reportKept: true);
|
||||||
await CleanModulesAsync(ins.ModuleIds, mode, report, reportKept: true);
|
await CleanModulesAsync(ins.ModuleIds, report, reportKept: true);
|
||||||
|
|
||||||
if (mode == WizardDeleteMode.Delete)
|
var groups = await CleanPermissionGroupsAsync(
|
||||||
{
|
Adopted([wizard?.PermissionGroupName], ins.PermissionGroupNames), report, reportKept: false);
|
||||||
var groups = await CleanPermissionGroupsAsync(
|
adoptedLanguageKeys.AddRange(groups);
|
||||||
Adopted([wizard?.PermissionGroupName], ins.PermissionGroupNames), mode, report, reportKept: false);
|
|
||||||
adoptedLanguageKeys.AddRange(groups);
|
|
||||||
|
|
||||||
await CleanModulesAsync(
|
await CleanModulesAsync(
|
||||||
Adopted([.. deletedMenus.Select(m => m.ModuleId), wizard?.MenuParentModuleId], ins.ModuleIds),
|
Adopted([.. deletedMenus.Select(m => m.ModuleId), wizard?.MenuParentModuleId], ins.ModuleIds),
|
||||||
mode, report, reportKept: false);
|
report, reportKept: false);
|
||||||
}
|
|
||||||
|
|
||||||
await CleanDataSourcesAsync(ins.DataSourceCodes, mode, report);
|
await CleanDataSourcesAsync(ins.DataSourceCodes, report);
|
||||||
await CleanLanguageKeysAsync(ins.LanguageKeys, report, reportKept: true);
|
await CleanLanguageKeysAsync(ins.LanguageKeys, report, reportKept: true);
|
||||||
await CleanLanguageKeysAsync(Adopted(adoptedLanguageKeys, ins.LanguageKeys), report, reportKept: false);
|
await CleanLanguageKeysAsync(Adopted(adoptedLanguageKeys, ins.LanguageKeys), report, reportKept: false);
|
||||||
|
|
||||||
await RemoveSeedRowsAsync(report);
|
|
||||||
|
|
||||||
foreach (var item in report)
|
foreach (var item in report)
|
||||||
{
|
{
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"Wizard '{WizardName}' {Mode}: {RecordType} '{Name}' -> {Status} {Reason} {UsedBy}",
|
"Wizard '{WizardName}' delete: {RecordType} '{Name}' -> {Status} {Reason} {UsedBy}",
|
||||||
wizard?.WizardName, mode, item.RecordType, item.Name, item.Status, item.Reason,
|
wizard?.WizardName, item.RecordType, item.Name, item.Status, item.Reason,
|
||||||
string.Join(", ", item.UsedBy.Select(u => $"{u.RecordType}:{u.Name}")));
|
string.Join(", ", item.UsedBy.Select(u => $"{u.RecordType}:{u.Name}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,10 +121,9 @@ public class WizardRecordCleaner(
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ekran baska bir ekranin alt formuysa silinmez; alanlari ve is akisi da ekranla birlikte
|
/// Ekran baska bir ekranin alt formuysa silinmez; alanlari ve is akisi da ekranla birlikte
|
||||||
/// kalir, aksi halde ust ekranin sekmesi bozulurdu. Duzenlemede deploy ekrani zaten yeniden
|
/// kalir, aksi halde ust ekranin sekmesi bozulurdu.
|
||||||
/// kurar, bu kontrol yapilmaz.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CleanListFormAsync(string listFormCode, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
private async Task CleanListFormAsync(string listFormCode, List<WizardDeleteLogItemDto> report)
|
||||||
{
|
{
|
||||||
var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
||||||
var fields = await repoListFormField.GetListAsync(a => a.ListFormCode == listFormCode);
|
var fields = await repoListFormField.GetListAsync(a => a.ListFormCode == listFormCode);
|
||||||
|
|
@ -136,7 +133,7 @@ public class WizardRecordCleaner(
|
||||||
{
|
{
|
||||||
Add(report, WizardRecordTypeEnum.ListForm, listFormCode, WizardDeleteStatusEnum.NotFound);
|
Add(report, WizardRecordTypeEnum.ListForm, listFormCode, WizardDeleteStatusEnum.NotFound);
|
||||||
}
|
}
|
||||||
else if (mode == WizardDeleteMode.Delete)
|
else
|
||||||
{
|
{
|
||||||
var quoted = Quote(listFormCode);
|
var quoted = Quote(listFormCode);
|
||||||
var parents = await repoListForm.GetListAsync(a =>
|
var parents = await repoListForm.GetListAsync(a =>
|
||||||
|
|
@ -171,15 +168,13 @@ public class WizardRecordCleaner(
|
||||||
{
|
{
|
||||||
await repoListForm.DeleteAsync(listForm, autoSave: true);
|
await repoListForm.DeleteAsync(listForm, autoSave: true);
|
||||||
Add(report, WizardRecordTypeEnum.ListForm, listFormCode, WizardDeleteStatusEnum.Deleted);
|
Add(report, WizardRecordTypeEnum.ListForm, listFormCode, WizardDeleteStatusEnum.Deleted);
|
||||||
|
DeleteSeedDataMirror(listForm.SeedFilePath, report);
|
||||||
if (mode == WizardDeleteMode.Delete)
|
|
||||||
DeleteSeedDataMirror(listForm.SeedFilePath, report);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ekranin veri aynasi (<c>ListForm.SeedFilePath</c>) ekransiz uygulanamaz; ekranla birlikte
|
/// Ekranin veri aynasi (<c>ListForm.SeedFilePath</c>) ekransiz uygulanamaz; ekranla birlikte
|
||||||
/// gider. Duzenlemede ekran ayni yolla yeniden kurulacagi icin dokunulmaz.
|
/// gider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void DeleteSeedDataMirror(string? seedFilePath, List<WizardDeleteLogItemDto> report)
|
private void DeleteSeedDataMirror(string? seedFilePath, List<WizardDeleteLogItemDto> report)
|
||||||
{
|
{
|
||||||
|
|
@ -222,17 +217,10 @@ public class WizardRecordCleaner(
|
||||||
/// Altinda baska menu kalan menu silinmez. Wizard'in kendi menusu ust menuden once silinir;
|
/// Altinda baska menu kalan menu silinmez. Wizard'in kendi menusu ust menuden once silinir;
|
||||||
/// ust menunun bos kalip kalmadigi ancak o zaman bilinir.
|
/// ust menunun bos kalip kalmadigi ancak o zaman bilinir.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<List<Menu>> CleanMenusAsync(List<string> codes, string? menuParentCode, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
private async Task<List<Menu>> CleanMenusAsync(List<string> codes, List<WizardDeleteLogItemDto> report)
|
||||||
{
|
{
|
||||||
var deleted = new List<Menu>();
|
var deleted = new List<Menu>();
|
||||||
var pending = new List<string>();
|
var pending = codes.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
foreach (var code in codes.Distinct(StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (mode == WizardDeleteMode.Redeploy && string.Equals(code, menuParentCode, StringComparison.OrdinalIgnoreCase))
|
|
||||||
AddRedeploy(report, WizardRecordTypeEnum.Menu, code);
|
|
||||||
else
|
|
||||||
pending.Add(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ust menu, alt menusu silindikten sonra bosalir; ilerleme durana kadar tekrar denenir.
|
// Ust menu, alt menusu silindikten sonra bosalir; ilerleme durana kadar tekrar denenir.
|
||||||
var progressed = true;
|
var progressed = true;
|
||||||
|
|
@ -306,11 +294,9 @@ public class WizardRecordCleaner(
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Kalan bir menu, ekran, alan, widget ya da alt yetki bir yetkiyi kullaniyorsa yetki silinmez.
|
/// Kalan bir menu, ekran, alan, widget ya da alt yetki bir yetkiyi kullaniyorsa yetki silinmez.
|
||||||
/// Alt yetkiler ust yetkiden once silinir. Silinen yetkinin atamalari da silinir: silme
|
/// Alt yetkiler ust yetkiden once silinir. Silinen yetkinin butun atamalari da silinir.
|
||||||
/// yolunda tum saglayicilarinki, duzenlemede yalnizca admin rolununki — diger rollerin
|
|
||||||
/// atamalari yeniden uretilen ayni adli yetkiye bagli kalir.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CleanPermissionsAsync(List<string> names, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
private async Task CleanPermissionsAsync(List<string> names, List<WizardDeleteLogItemDto> report)
|
||||||
{
|
{
|
||||||
var pending = names.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
var pending = names.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
var blockers = new Dictionary<string, List<WizardRecordRefDto>>(StringComparer.OrdinalIgnoreCase);
|
var blockers = new Dictionary<string, List<WizardRecordRefDto>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
@ -337,7 +323,7 @@ public class WizardRecordCleaner(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await DeletePermissionGrantsAsync(name, mode, report);
|
await DeletePermissionGrantsAsync(name, report);
|
||||||
await repoPerm.DeleteAsync(permission, autoSave: true);
|
await repoPerm.DeleteAsync(permission, autoSave: true);
|
||||||
Add(report, WizardRecordTypeEnum.Permission, name, WizardDeleteStatusEnum.Deleted);
|
Add(report, WizardRecordTypeEnum.Permission, name, WizardDeleteStatusEnum.Deleted);
|
||||||
pending.Remove(name);
|
pending.Remove(name);
|
||||||
|
|
@ -369,12 +355,9 @@ public class WizardRecordCleaner(
|
||||||
return usedBy;
|
return usedBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeletePermissionGrantsAsync(string name, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
private async Task DeletePermissionGrantsAsync(string name, List<WizardDeleteLogItemDto> report)
|
||||||
{
|
{
|
||||||
var grants = mode == WizardDeleteMode.Delete
|
var grants = await repoPermGrant.GetListAsync(a => a.Name == name);
|
||||||
? await repoPermGrant.GetListAsync(a => a.Name == name)
|
|
||||||
: await repoPermGrant.GetListAsync(a =>
|
|
||||||
a.Name == name && a.ProviderName == "R" && a.ProviderKey == PlatformConsts.AbpIdentity.User.AdminRoleName);
|
|
||||||
|
|
||||||
if (grants.Count == 0)
|
if (grants.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
@ -390,17 +373,11 @@ public class WizardRecordCleaner(
|
||||||
/// <paramref name="reportKept"/> kapaliyken kayit wizard'in degil, yalnizca baglandigi kaptir:
|
/// <paramref name="reportKept"/> kapaliyken kayit wizard'in degil, yalnizca baglandigi kaptir:
|
||||||
/// bulunamamasi ya da kullanimda kalmasi rapora yazilmaz. Silinen grup adlarini dondurur.
|
/// bulunamamasi ya da kullanimda kalmasi rapora yazilmaz. Silinen grup adlarini dondurur.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<List<string>> CleanPermissionGroupsAsync(List<string> names, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report, bool reportKept)
|
private async Task<List<string>> CleanPermissionGroupsAsync(List<string> names, List<WizardDeleteLogItemDto> report, bool reportKept)
|
||||||
{
|
{
|
||||||
var deleted = new List<string>();
|
var deleted = new List<string>();
|
||||||
foreach (var name in names.Distinct(StringComparer.OrdinalIgnoreCase))
|
foreach (var name in names.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (mode == WizardDeleteMode.Redeploy)
|
|
||||||
{
|
|
||||||
AddRedeploy(report, WizardRecordTypeEnum.PermissionGroup, name);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var group = await repoPermGroup.FirstOrDefaultAsync(a => a.Name == name);
|
var group = await repoPermGroup.FirstOrDefaultAsync(a => a.Name == name);
|
||||||
if (group == null)
|
if (group == null)
|
||||||
{
|
{
|
||||||
|
|
@ -432,22 +409,13 @@ public class WizardRecordCleaner(
|
||||||
/// Modul; menuler, yetkilerin modul listesi ve kiracilarin modul listesi tarafindan kullanilir.
|
/// Modul; menuler, yetkilerin modul listesi ve kiracilarin modul listesi tarafindan kullanilir.
|
||||||
/// <paramref name="reportKept"/> izin gruplarindakiyle aynidir.
|
/// <paramref name="reportKept"/> izin gruplarindakiyle aynidir.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CleanModulesAsync(List<string> ids, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report, bool reportKept)
|
private async Task CleanModulesAsync(List<string> ids, List<WizardDeleteLogItemDto> report, bool reportKept)
|
||||||
{
|
{
|
||||||
if (ids.Count == 0)
|
if (ids.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
List<PermissionDefinitionRecord>? permissions = null;
|
|
||||||
List<Volo.Abp.TenantManagement.Tenant>? tenants = null;
|
|
||||||
|
|
||||||
foreach (var id in ids.Distinct(StringComparer.OrdinalIgnoreCase))
|
foreach (var id in ids.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (mode == WizardDeleteMode.Redeploy)
|
|
||||||
{
|
|
||||||
AddRedeploy(report, WizardRecordTypeEnum.Module, id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var module = await repoModule.FindAsync(id);
|
var module = await repoModule.FindAsync(id);
|
||||||
if (module == null)
|
if (module == null)
|
||||||
{
|
{
|
||||||
|
|
@ -456,19 +424,7 @@ public class WizardRecordCleaner(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modul listeleri ekstra ozellikte metin olarak durur; sorguyla suzulemez, bellekte bakilir.
|
var usedBy = await FindModuleUsagesAsync(id);
|
||||||
permissions ??= await repoPerm.GetListAsync();
|
|
||||||
tenants ??= await tenantRepository.GetListAsync();
|
|
||||||
|
|
||||||
var usedBy = new List<WizardRecordRefDto>();
|
|
||||||
usedBy.AddRange((await repoMenu.GetListAsync(a => a.ModuleId == id))
|
|
||||||
.Select(m => Ref(WizardRecordTypeEnum.Menu, m.Code)));
|
|
||||||
usedBy.AddRange(permissions
|
|
||||||
.Where(p => p.GetModules().Contains(id, StringComparer.OrdinalIgnoreCase))
|
|
||||||
.Select(p => Ref(WizardRecordTypeEnum.Permission, p.Name)));
|
|
||||||
usedBy.AddRange(tenants
|
|
||||||
.Where(t => t.GetModules().Contains(id, StringComparer.OrdinalIgnoreCase))
|
|
||||||
.Select(t => Ref(WizardRecordTypeEnum.Tenant, t.Name)));
|
|
||||||
|
|
||||||
if (usedBy.Count > 0)
|
if (usedBy.Count > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -482,16 +438,26 @@ public class WizardRecordCleaner(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CleanDataSourcesAsync(List<string> codes, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
private async Task<List<WizardRecordRefDto>> FindModuleUsagesAsync(string id)
|
||||||
|
{
|
||||||
|
// Modul listeleri ekstra ozellikte metin olarak durur; sorguyla suzulemez, bellekte bakilir.
|
||||||
|
var usedBy = new List<WizardRecordRefDto>();
|
||||||
|
usedBy.AddRange((await repoMenu.GetListAsync(a => a.ModuleId == id))
|
||||||
|
.Select(m => Ref(WizardRecordTypeEnum.Menu, m.Code)));
|
||||||
|
usedBy.AddRange((await repoPerm.GetListAsync())
|
||||||
|
.Where(p => p.GetModules().Contains(id, StringComparer.OrdinalIgnoreCase))
|
||||||
|
.Select(p => Ref(WizardRecordTypeEnum.Permission, p.Name)));
|
||||||
|
usedBy.AddRange((await tenantRepository.GetListAsync())
|
||||||
|
.Where(t => t.GetModules().Contains(id, StringComparer.OrdinalIgnoreCase))
|
||||||
|
.Select(t => Ref(WizardRecordTypeEnum.Tenant, t.Name)));
|
||||||
|
|
||||||
|
return usedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CleanDataSourcesAsync(List<string> codes, List<WizardDeleteLogItemDto> report)
|
||||||
{
|
{
|
||||||
foreach (var code in codes.Distinct(StringComparer.OrdinalIgnoreCase))
|
foreach (var code in codes.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (mode == WizardDeleteMode.Redeploy)
|
|
||||||
{
|
|
||||||
AddRedeploy(report, WizardRecordTypeEnum.DataSource, code);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dataSource = await repoDataSource.FirstOrDefaultAsync(a => a.Code == code);
|
var dataSource = await repoDataSource.FirstOrDefaultAsync(a => a.Code == code);
|
||||||
if (dataSource == null)
|
if (dataSource == null)
|
||||||
{
|
{
|
||||||
|
|
@ -539,17 +505,7 @@ public class WizardRecordCleaner(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var usedBy = new List<WizardRecordRefDto>();
|
var usedBy = await FindLanguageKeyUsagesAsync(key);
|
||||||
usedBy.AddRange((await repoMenu.GetListAsync(a => a.DisplayName == key))
|
|
||||||
.Select(m => Ref(WizardRecordTypeEnum.Menu, m.Code)));
|
|
||||||
usedBy.AddRange((await repoPerm.GetListAsync(a => a.DisplayName == key))
|
|
||||||
.Select(p => Ref(WizardRecordTypeEnum.Permission, p.Name)));
|
|
||||||
usedBy.AddRange((await repoPermGroup.GetListAsync(a => a.DisplayName == key))
|
|
||||||
.Select(g => Ref(WizardRecordTypeEnum.PermissionGroup, g.Name)));
|
|
||||||
usedBy.AddRange((await repoListForm.GetListAsync(a => a.Title == key || a.Description == key))
|
|
||||||
.Select(l => Ref(WizardRecordTypeEnum.ListForm, l.ListFormCode)));
|
|
||||||
usedBy.AddRange((await repoListFormField.GetListAsync(a => a.CaptionName == key))
|
|
||||||
.Select(f => Ref(WizardRecordTypeEnum.ListFormField, FieldName(f))));
|
|
||||||
|
|
||||||
if (usedBy.Count > 0)
|
if (usedBy.Count > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -567,22 +523,58 @@ public class WizardRecordCleaner(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<List<WizardRecordRefDto>> FindLanguageKeyUsagesAsync(string key)
|
||||||
|
{
|
||||||
|
var usedBy = new List<WizardRecordRefDto>();
|
||||||
|
usedBy.AddRange((await repoMenu.GetListAsync(a => a.DisplayName == key))
|
||||||
|
.Select(m => Ref(WizardRecordTypeEnum.Menu, m.Code)));
|
||||||
|
usedBy.AddRange((await repoPerm.GetListAsync(a => a.DisplayName == key))
|
||||||
|
.Select(p => Ref(WizardRecordTypeEnum.Permission, p.Name)));
|
||||||
|
usedBy.AddRange((await repoPermGroup.GetListAsync(a => a.DisplayName == key))
|
||||||
|
.Select(g => Ref(WizardRecordTypeEnum.PermissionGroup, g.Name)));
|
||||||
|
usedBy.AddRange((await repoListForm.GetListAsync(a => a.Title == key || a.Description == key))
|
||||||
|
.Select(l => Ref(WizardRecordTypeEnum.ListForm, l.ListFormCode)));
|
||||||
|
usedBy.AddRange((await repoListFormField.GetListAsync(a => a.CaptionName == key))
|
||||||
|
.Select(f => Ref(WizardRecordTypeEnum.ListFormField, FieldName(f))));
|
||||||
|
|
||||||
|
return usedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir kaydi veritabaninda kullanan kayitlar; silme kurallariyla aynidir. Migrate edilmemis
|
||||||
|
/// wizard'in satiri seed dosyasindan cikarilirken de bu kontrolden gecer: kayit veritabaninda
|
||||||
|
/// baska bir yerden kullaniliyorsa satiri kalir.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<WizardRecordRefDto>> FindUsagesAsync(WizardRecordTypeEnum type, string name) => type switch
|
||||||
|
{
|
||||||
|
WizardRecordTypeEnum.Menu => [.. (await repoMenu.GetListAsync(a => a.ParentCode == name))
|
||||||
|
.Select(m => Ref(WizardRecordTypeEnum.Menu, m.Code))],
|
||||||
|
WizardRecordTypeEnum.Permission => await FindPermissionUsagesAsync(name),
|
||||||
|
WizardRecordTypeEnum.PermissionGroup => [.. (await repoPerm.GetListAsync(a => a.GroupName == name))
|
||||||
|
.Select(p => Ref(WizardRecordTypeEnum.Permission, p.Name))],
|
||||||
|
WizardRecordTypeEnum.Module => await FindModuleUsagesAsync(name),
|
||||||
|
WizardRecordTypeEnum.LanguageKey => await FindLanguageKeyUsagesAsync(name),
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
|
||||||
// ── Seed dosyalari ─────────────────────────────────────────────────────────────
|
// ── Seed dosyalari ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Silinen kayitlarin ekran seed dosyalarindaki satirlari. Wizard bu dosyalara yazmaz; satir,
|
/// Kaydin seed dosyalarindaki satirlari. Wizard menu/yetki/dil kayitlarini bu dosyalara yazar ve
|
||||||
/// kayit menu/modul/yetki ekranlarindan duzenlendiginde senkronizasyonla girer ve kayit silindikten
|
/// migrate onlari oradan kurar; silinen kaydin satiri dosyada kalirsa migrate onu geri getirir.
|
||||||
/// sonra dosyada kalirsa migrate onu geri getirir.
|
/// Veritabaninda bulunmayan kayit henuz migrate edilmemistir ve yalnizca dosyada yasar; o da
|
||||||
|
/// cikarilir. Kullanimda oldugu icin birakilan kaydin satirina dokunulmaz.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task RemoveSeedRowsAsync(List<WizardDeleteLogItemDto> report)
|
public async Task RemoveSeedRowsAsync(List<WizardDeleteLogItemDto> report)
|
||||||
{
|
{
|
||||||
List<string> Deleted(WizardRecordTypeEnum type) =>
|
List<string> Removed(WizardRecordTypeEnum type) =>
|
||||||
[.. report.Where(i => i.RecordType == type && i.Status == WizardDeleteStatusEnum.Deleted).Select(i => i.Name)];
|
[.. report.Where(i => i.RecordType == type && i.Status is WizardDeleteStatusEnum.Deleted or WizardDeleteStatusEnum.NotFound).Select(i => i.Name)];
|
||||||
|
|
||||||
await seedDataSynchronizer.RemoveRowsByFieldAsync(PlatformConsts.AppCodes.Menus.Menu, nameof(Menu.Code), Deleted(WizardRecordTypeEnum.Menu));
|
await seedDataSynchronizer.RemoveRowsByFieldAsync(PlatformConsts.AppCodes.Menus.Menu, nameof(Menu.Code), Removed(WizardRecordTypeEnum.Menu));
|
||||||
await seedDataSynchronizer.RemoveRowsByFieldAsync(PlatformConsts.AppCodes.Menus.Module, nameof(Module.Id), Deleted(WizardRecordTypeEnum.Module));
|
await seedDataSynchronizer.RemoveRowsByFieldAsync(PlatformConsts.AppCodes.Menus.Module, nameof(Module.Id), Removed(WizardRecordTypeEnum.Module));
|
||||||
await seedDataSynchronizer.RemoveRowsByFieldAsync(SeedConsts.AbpIdentity.Permissions.Default, nameof(PermissionDefinitionRecord.Name), Deleted(WizardRecordTypeEnum.Permission));
|
await seedDataSynchronizer.RemoveRowsByFieldAsync(SeedConsts.AbpIdentity.Permissions.Default, nameof(PermissionDefinitionRecord.Name), Removed(WizardRecordTypeEnum.Permission));
|
||||||
await seedDataSynchronizer.RemoveRowsByFieldAsync(SeedConsts.AbpIdentity.PermissionGroups.Default, nameof(PermissionGroupDefinitionRecord.Name), Deleted(WizardRecordTypeEnum.PermissionGroup));
|
await seedDataSynchronizer.RemoveRowsByFieldAsync(SeedConsts.AbpIdentity.PermissionGroups.Default, nameof(PermissionGroupDefinitionRecord.Name), Removed(WizardRecordTypeEnum.PermissionGroup));
|
||||||
|
await seedDataSynchronizer.RemoveRowsByFieldAsync(PlatformConsts.AppCodes.Languages.LanguageText, nameof(LanguageText.Key), Removed(WizardRecordTypeEnum.LanguageKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Wizard'in kendi urettikleri disinda kalan, bos olmayan adaylar.</summary>
|
/// <summary>Wizard'in kendi urettikleri disinda kalan, bos olmayan adaylar.</summary>
|
||||||
|
|
@ -618,25 +610,4 @@ public class WizardRecordCleaner(
|
||||||
.Take(MaxUsedBy)],
|
.Take(MaxUsedBy)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddRedeploy(List<WizardDeleteLogItemDto> report, WizardRecordTypeEnum type, string name)
|
|
||||||
{
|
|
||||||
report.Add(new WizardDeleteLogItemDto
|
|
||||||
{
|
|
||||||
RecordType = type,
|
|
||||||
Name = name,
|
|
||||||
Status = WizardDeleteStatusEnum.Kept,
|
|
||||||
Reason = WizardDeleteReasonEnum.Redeploy,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Wizard kayitlarinin hangi amacla silindigi.</summary>
|
|
||||||
public enum WizardDeleteMode
|
|
||||||
{
|
|
||||||
/// <summary>Wizard Manager'dan silme: kullanilmayan her kayit gider.</summary>
|
|
||||||
Delete,
|
|
||||||
|
|
||||||
/// <summary>Duzenleme: kayitlar hemen ardindan yeniden uretilir, paylasilan kaplar korunur.</summary>
|
|
||||||
Redeploy,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.ListForms;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wizard'in yazdigi platform seed dosyalari: menu, modul, izin grubu, yetki ve dil metni.
|
||||||
|
/// <para>
|
||||||
|
/// Deploy veritabanina yazmaz; ekranin menu/yetki/dil kayitlari bu dosyalara satir olarak girer ve
|
||||||
|
/// migrate onlari <c>ListFormSeedDataApplier</c> ile ekler. Dosyalar bir kez okunur, bellekte
|
||||||
|
/// degistirilir ve yalnizca degisenler yazilir.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WizardSeedRowStore
|
||||||
|
{
|
||||||
|
/// <summary>Dosyanin ekran kodu, satir anahtari ve dosya yoksa yazilacak uygulama sirasi.</summary>
|
||||||
|
private sealed record FileSpec(string ListFormCode, string KeyFieldName, int Order);
|
||||||
|
|
||||||
|
public static readonly string MenuFile = PlatformConsts.AppCodes.Menus.Menu;
|
||||||
|
public static readonly string ModuleFile = PlatformConsts.AppCodes.Menus.Module;
|
||||||
|
public static readonly string PermissionGroupFile = SeedConsts.AbpIdentity.PermissionGroups.Default;
|
||||||
|
public static readonly string PermissionFile = SeedConsts.AbpIdentity.Permissions.Default;
|
||||||
|
public static readonly string LanguageTextFile = PlatformConsts.AppCodes.Languages.LanguageText;
|
||||||
|
|
||||||
|
// Sira, kapsam klasorundeki mevcut dosyalarla aynidir: once tanim, sonra ona referans veren satir.
|
||||||
|
private static readonly FileSpec[] Specs =
|
||||||
|
[
|
||||||
|
new(ModuleFile, "Id", 2),
|
||||||
|
new(MenuFile, "Code", 3),
|
||||||
|
new(PermissionGroupFile, "Name", 1),
|
||||||
|
new(PermissionFile, "Name", 2),
|
||||||
|
new(LanguageTextFile, "Id", 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>Wizard'in satir yazdigi ekran kodlari; import bu dosyalari veritabanina uygulamaz.</summary>
|
||||||
|
public static IReadOnlyList<string> ListFormCodes { get; } = [.. Specs.Select(s => s.ListFormCode)];
|
||||||
|
|
||||||
|
private readonly Dictionary<string, (string Path, ListFormSeedDataFileDto File)> files = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly HashSet<string> dirty = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private WizardSeedRowStore()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Kapsam klasorundeki dosyalari okur; olmayan dosya bos olarak baslar ve ilk yazimda olusur.</summary>
|
||||||
|
public static async Task<WizardSeedRowStore> LoadAsync(string scopeRoot)
|
||||||
|
{
|
||||||
|
var store = new WizardSeedRowStore();
|
||||||
|
|
||||||
|
foreach (var spec in Specs)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(scopeRoot, SeedPathResolver.GetDataFilePath(spec.ListFormCode).Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
|
||||||
|
var file = File.Exists(path)
|
||||||
|
? JsonSerializer.Deserialize<ListFormSeedDataFileDto>(await File.ReadAllTextAsync(path), ListFormSeedDataFileDto.JsonOptions)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
file ??= new ListFormSeedDataFileDto { ListFormCode = spec.ListFormCode, KeyFieldName = spec.KeyFieldName, Order = spec.Order };
|
||||||
|
file.Rows ??= [];
|
||||||
|
|
||||||
|
store.files[spec.ListFormCode] = (path, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<Dictionary<string, object>> Rows(string listFormCode) => files[listFormCode].File.Rows;
|
||||||
|
|
||||||
|
public Dictionary<string, object>? Find(string listFormCode, string field, string? value) =>
|
||||||
|
string.IsNullOrWhiteSpace(value)
|
||||||
|
? null
|
||||||
|
: Rows(listFormCode).FirstOrDefault(row => Matches(row, field, value));
|
||||||
|
|
||||||
|
public bool Contains(string listFormCode, string field, string? value) => Find(listFormCode, field, value) != null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Satiri <paramref name="matchFields"/> alanlarinin degerleriyle eslesen satirin yerine yazar,
|
||||||
|
/// yoksa sona ekler. Var olan satirin bu listede olmayan alanlari (kimlik gibi) korunur; dil
|
||||||
|
/// metni is anahtariyla eslesir, dosyadaki kimligi degismez.
|
||||||
|
/// </summary>
|
||||||
|
public void Upsert(string listFormCode, Dictionary<string, object> row, params string[] matchFields)
|
||||||
|
{
|
||||||
|
var file = files[listFormCode].File;
|
||||||
|
var index = file.Rows.FindIndex(existing => matchFields.All(f => Matches(existing, f, Text(row, f))));
|
||||||
|
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
file.Rows.Add(row);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var merged = new Dictionary<string, object>(file.Rows[index]);
|
||||||
|
foreach (var item in row)
|
||||||
|
{
|
||||||
|
// Satirin anahtari is anahtarindan ayriysa (dil metninin kimligi) dosyadaki deger kalir.
|
||||||
|
if (string.Equals(item.Key, file.KeyFieldName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !matchFields.Contains(item.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
&& merged.ContainsKey(item.Key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
merged[item.Key] = item.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Rows[index] = merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirty.Add(listFormCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <paramref name="field"/> degeri <paramref name="values"/> icinde olan satirlari cikarir;
|
||||||
|
/// <paramref name="keep"/> true donen satir birakilir. Cikarilan satirlarin alan degerlerini
|
||||||
|
/// dondurur.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Remove(string listFormCode, string field, IEnumerable<string> values, Func<Dictionary<string, object>, bool>? keep = null)
|
||||||
|
{
|
||||||
|
var wanted = values.Where(v => !string.IsNullOrWhiteSpace(v)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var rows = files[listFormCode].File.Rows;
|
||||||
|
var removed = rows.Where(row => wanted.Contains(Text(row, field) ?? string.Empty) && keep?.Invoke(row) != true).ToList();
|
||||||
|
|
||||||
|
if (removed.Count == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
rows.RemoveAll(removed.Contains);
|
||||||
|
dirty.Add(listFormCode);
|
||||||
|
|
||||||
|
return [.. removed.Select(row => Text(row, field)!).Distinct(StringComparer.OrdinalIgnoreCase)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wizard'in sahip oldugu satirlari cikarir; baska bir yerde kullanilan satir kalir. Kullanim iki
|
||||||
|
/// yerden okunur: seed dosyalarinda listede olmayan bir satirin ona baglanmasi (alt menu, alt yetki,
|
||||||
|
/// menunun istedigi yetki, izin grubundaki yetki, modulu kullanan menu/yetki, dil anahtarini gorunen
|
||||||
|
/// ad olarak kullanan menu/yetki/grup) ve <paramref name="usedElsewhere"/> ile cagiranin verdigi
|
||||||
|
/// dis kullanim (veritabani). Siralama bagimlilik yonundedir — once kullanan, sonra kullanilan —
|
||||||
|
/// ve her kontrol o ana kadar kalan satirlara karsi yapilir.
|
||||||
|
/// </summary>
|
||||||
|
public List<(WizardRecordTypeEnum Type, string Name, bool Removed)> RemoveOwned(
|
||||||
|
WizardInsertedRecordsDto records,
|
||||||
|
Func<WizardRecordTypeEnum, string, bool>? usedElsewhere = null)
|
||||||
|
{
|
||||||
|
static bool Same(string? a, string? b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
|
||||||
|
static bool In(List<string> list, string? value) => list.Contains(value ?? string.Empty, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var result = new List<(WizardRecordTypeEnum, string, bool)>();
|
||||||
|
|
||||||
|
void Apply(WizardRecordTypeEnum type, string listFormCode, string field, List<string> names, Func<Dictionary<string, object>, bool> usedInFiles)
|
||||||
|
{
|
||||||
|
var wanted = names.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
var removed = Remove(listFormCode, field, wanted, row =>
|
||||||
|
usedInFiles(row) || usedElsewhere?.Invoke(type, Text(row, field) ?? string.Empty) == true);
|
||||||
|
|
||||||
|
foreach (var name in wanted.Where(n => Contains(listFormCode, field, n)))
|
||||||
|
result.Add((type, name, false));
|
||||||
|
|
||||||
|
result.AddRange(removed.Select(n => (type, n, true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var menuCodes = records.MenuCodes ?? [];
|
||||||
|
var permissionNames = records.PermissionNames ?? [];
|
||||||
|
var groupNames = records.PermissionGroupNames ?? [];
|
||||||
|
|
||||||
|
Apply(WizardRecordTypeEnum.Menu, MenuFile, "Code", menuCodes, menu =>
|
||||||
|
Rows(MenuFile).Any(child => Same(Text(child, "ParentCode"), Text(menu, "Code")) && !In(menuCodes, Text(child, "Code"))));
|
||||||
|
|
||||||
|
Apply(WizardRecordTypeEnum.Permission, PermissionFile, "Name", permissionNames, permission =>
|
||||||
|
{
|
||||||
|
var name = Text(permission, "Name");
|
||||||
|
return Rows(PermissionFile).Any(p => Same(Text(p, "ParentName"), name) && !In(permissionNames, Text(p, "Name")))
|
||||||
|
|| Rows(MenuFile).Any(m => Same(Text(m, "RequiredPermissionName"), name));
|
||||||
|
});
|
||||||
|
|
||||||
|
Apply(WizardRecordTypeEnum.PermissionGroup, PermissionGroupFile, "Name", groupNames, group =>
|
||||||
|
Rows(PermissionFile).Any(p => Same(Text(p, "GroupName"), Text(group, "Name"))));
|
||||||
|
|
||||||
|
Apply(WizardRecordTypeEnum.Module, ModuleFile, "Id", records.ModuleIds ?? [], module =>
|
||||||
|
{
|
||||||
|
var id = Text(module, "Id");
|
||||||
|
return Rows(MenuFile).Any(m => Same(Text(m, "ModuleId"), id))
|
||||||
|
|| Rows(PermissionFile).Any(p =>
|
||||||
|
(Text(p, PlatformConsts.Permissions.Modules) ?? string.Empty)
|
||||||
|
.Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Contains(id, StringComparer.OrdinalIgnoreCase));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dil anahtari en son: kalan menu, yetki ya da grup satiri onu gorunen ad olarak gosteriyorsa kalir.
|
||||||
|
Apply(WizardRecordTypeEnum.LanguageKey, LanguageTextFile, "Key", records.LanguageKeys ?? [], text =>
|
||||||
|
{
|
||||||
|
var key = Text(text, "Key");
|
||||||
|
return Rows(MenuFile).Any(m => Same(Text(m, "DisplayName"), key))
|
||||||
|
|| Rows(PermissionFile).Any(p => Same(Text(p, "DisplayName"), key))
|
||||||
|
|| Rows(PermissionGroupFile).Any(g => Same(Text(g, "DisplayName"), key));
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync()
|
||||||
|
{
|
||||||
|
foreach (var listFormCode in dirty)
|
||||||
|
{
|
||||||
|
var (path, file) = files[listFormCode];
|
||||||
|
file.GeneratedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
await File.WriteAllTextAsync(path, ListFormSeedDataFileDto.Serialize(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
dirty.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? Text(Dictionary<string, object>? row, string field) => WizardExportCollector.GetRowText(row, field);
|
||||||
|
|
||||||
|
public static int Int(Dictionary<string, object>? row, string field) =>
|
||||||
|
int.TryParse(Text(row, field), out var value) ? value : 0;
|
||||||
|
|
||||||
|
private static bool Matches(Dictionary<string, object> row, string field, string? value) =>
|
||||||
|
string.Equals(Text(row, field), value, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,14 @@ public class WizardSeedSynchronizer(
|
||||||
if (seed == null)
|
if (seed == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Bekleyen kurulumu olan dosya ekranin bir sonraki surumunu tasir; veritabanindaki (eski)
|
||||||
|
// ekrani dosyaya yazmak migrate edilmemis duzenlemeyi silerdi.
|
||||||
|
if (seed.Pending != null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Wizard seed file for '{ListFormCode}' is waiting for DB Migrate, designer changes were not written to it.", listFormCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
||||||
if (listForm == null)
|
if (listForm == null)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
@ -8,86 +7,64 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Sozsoft.Languages.Entities;
|
using Sozsoft.Languages.Entities;
|
||||||
using Sozsoft.Languages.Languages;
|
|
||||||
using Sozsoft.Platform.Entities;
|
using Sozsoft.Platform.Entities;
|
||||||
using Sozsoft.Platform.Enums;
|
using Sozsoft.Platform.Enums;
|
||||||
using Sozsoft.Platform.Extensions;
|
|
||||||
using Sozsoft.Platform.ListForms;
|
using Sozsoft.Platform.ListForms;
|
||||||
using Volo.Abp.Data;
|
using Volo.Abp.Data;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
using Volo.Abp.MultiTenancy;
|
|
||||||
using Volo.Abp.PermissionManagement;
|
using Volo.Abp.PermissionManagement;
|
||||||
using static Sozsoft.Platform.PlatformConsts;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Data.Seeds;
|
namespace Sozsoft.Platform.Data.Seeds;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wizard ile oluşturulan konfigürasyonları Seeds/{host|tenants/{tenantId}}/wizard/*.json
|
/// Wizard ile uretilen ekranlari Seeds/{host|tenants/{tenantId}}/wizard/*.json dosyalarindan kurar
|
||||||
/// dosyalarından okuyarak veritabanına aktarır (kapsam, seed edilen veritabanını izler).
|
/// (kapsam, seed edilen veritabanini izler).
|
||||||
/// Wizard çalıştıktan sonra oluşturulan JSON dosyaları projeye commit edildikten sonra,
|
/// <para>
|
||||||
/// veritabanı silinip yeniden oluşturulduğunda bu seeder tüm wizard konfigürasyonlarını geri yükler.
|
/// Deploy veritabanina yazmaz; bu seeder wizard'in kurulumunun tek yeridir. Dosya yalnizca ekrana
|
||||||
|
/// ozgu kayitlari tasir: veri kaynagi, ListForm, alanlar ve is akisi. Yetkiler hicbir role atanmaz;
|
||||||
|
/// hangi rolun ekrani gorecegi rol ekranindan secilir.
|
||||||
|
/// Menu, modul, izin grubu, yetki ve dil metni satirlari platform seed dosyalarindadir
|
||||||
|
/// (<c>data/*.json</c>) ve bu seeder'dan sonra <c>ListFormSeedDataApplier</c> tarafindan eklenir.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Uygulama kurali: kurulu olmayan ekran kurulur, kurulu olana dokunulmaz. Bekleyen kurulumu olan
|
||||||
|
/// dosyada (<see cref="WizardSeedFileDto.Pending"/> — deploy ya da duzenleme sonrasi) once wizard'in
|
||||||
|
/// onceki ve guncel kayitlari veritabanindan kaldirilir, sonra ekran yeniden kurulur ve bekleyen
|
||||||
|
/// bolum dosyadan silinir; kaldirilan satirlari applier dosyadaki guncel halleriyle geri ekler.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
public class WizardDataSeeder(
|
||||||
|
IRepository<LanguageKey, Guid> repoLangKey,
|
||||||
|
IRepository<LanguageText, Guid> repoLangText,
|
||||||
|
IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup,
|
||||||
|
IRepository<PermissionDefinitionRecord, Guid> repoPerm,
|
||||||
|
IRepository<Menu, Guid> repoMenu,
|
||||||
|
IRepository<DataSource, Guid> repoDataSource,
|
||||||
|
IRepository<ListForm, Guid> repoListForm,
|
||||||
|
IRepository<ListFormField, Guid> repoListFormField,
|
||||||
|
IRepository<ListFormWorkflow, string> repoListFormWorkflow,
|
||||||
|
IRepository<PermissionGrant, Guid> repoPermGrant,
|
||||||
|
WizardInstallationChecker installationChecker,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<WizardDataSeeder> logger) : IDataSeedContributor, ITransientDependency
|
||||||
{
|
{
|
||||||
private readonly IRepository<LanguageKey, Guid> _repoLangKey;
|
|
||||||
private readonly IRepository<LanguageText, Guid> _repoLangText;
|
|
||||||
private readonly IRepository<PermissionGroupDefinitionRecord, Guid> _repoPermGroup;
|
|
||||||
private readonly IRepository<PermissionDefinitionRecord, Guid> _repoPerm;
|
|
||||||
private readonly IRepository<Menu, Guid> _repoMenu;
|
|
||||||
private readonly IRepository<Module, string> _repoModule;
|
|
||||||
private readonly IRepository<DataSource, Guid> _repoDataSource;
|
|
||||||
private readonly IRepository<ListForm, Guid> _repoListForm;
|
|
||||||
private readonly IRepository<ListFormField, Guid> _repoListFormField;
|
|
||||||
private readonly IRepository<ListFormWorkflow, string> _repoListFormWorkflow;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly ILogger<WizardDataSeeder> _logger;
|
|
||||||
|
|
||||||
private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage;
|
|
||||||
private readonly string _appName = PlatformConsts.AppName;
|
private readonly string _appName = PlatformConsts.AppName;
|
||||||
|
|
||||||
public WizardDataSeeder(
|
|
||||||
IRepository<LanguageKey, Guid> repoLangKey,
|
|
||||||
IRepository<LanguageText, Guid> repoLangText,
|
|
||||||
IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup,
|
|
||||||
IRepository<PermissionDefinitionRecord, Guid> repoPerm,
|
|
||||||
IRepository<Menu, Guid> repoMenu,
|
|
||||||
IRepository<Module, string> repoModule,
|
|
||||||
IRepository<DataSource, Guid> repoDataSource,
|
|
||||||
IRepository<ListForm, Guid> repoListForm,
|
|
||||||
IRepository<ListFormField, Guid> repoListFormField,
|
|
||||||
IRepository<ListFormWorkflow, string> repoListFormWorkflow,
|
|
||||||
IConfiguration configuration,
|
|
||||||
ILogger<WizardDataSeeder> logger)
|
|
||||||
{
|
|
||||||
_repoLangKey = repoLangKey;
|
|
||||||
_repoLangText = repoLangText;
|
|
||||||
_repoPermGroup = repoPermGroup;
|
|
||||||
_repoPerm = repoPerm;
|
|
||||||
_repoMenu = repoMenu;
|
|
||||||
_repoModule = repoModule;
|
|
||||||
_repoDataSource = repoDataSource;
|
|
||||||
_repoListForm = repoListForm;
|
|
||||||
_repoListFormField = repoListFormField;
|
|
||||||
_repoListFormWorkflow = repoListFormWorkflow;
|
|
||||||
_configuration = configuration;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SeedAsync(DataSeedContext context)
|
public async Task SeedAsync(DataSeedContext context)
|
||||||
{
|
{
|
||||||
var scopeFolderName = SeedPathResolver.GetScopeFolderName(context.TenantId);
|
var scopeFolderName = SeedPathResolver.GetScopeFolderName(context.TenantId);
|
||||||
var wizardDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, SeedPathResolver.WizardFolder);
|
var wizardDataPath = SeedPathResolver.GetScopePath(configuration, context.TenantId, SeedPathResolver.WizardFolder);
|
||||||
if (!Directory.Exists(wizardDataPath))
|
if (!Directory.Exists(wizardDataPath))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Seeds/{Scope}/wizard directory not found, skipping.", scopeFolderName);
|
logger.LogInformation("Seeds/{Scope}/wizard directory not found, skipping.", scopeFolderName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonFiles = Directory.GetFiles(wizardDataPath, "*.json");
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +78,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
var seed = WizardSeedFileDto.FromSeedJson(await File.ReadAllTextAsync(filePath));
|
var seed = WizardSeedFileDto.FromSeedJson(await File.ReadAllTextAsync(filePath));
|
||||||
if (seed?.Wizard == null)
|
if (seed?.Wizard == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Invalid file skipped: {FilePath}", filePath);
|
logger.LogWarning("Invalid file skipped: {FilePath}", filePath);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,11 +86,11 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, $"Error - {filePath}: {ex.Message}");
|
logger.LogError(ex, "Error - {FilePath}: {Message}", filePath, ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("WizardDataSeeder started. {Count} files to be processed.", seedFiles.Count);
|
logger.LogInformation("WizardDataSeeder started. {Count} files to be processed.", seedFiles.Count);
|
||||||
|
|
||||||
foreach (var (filePath, seedFile) in seedFiles
|
foreach (var (filePath, seedFile) in seedFiles
|
||||||
.OrderBy(x => x.Seed.Wizard.SeededAt)
|
.OrderBy(x => x.Seed.Wizard.SeededAt)
|
||||||
|
|
@ -122,269 +99,100 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
var fileName = Path.GetFileName(filePath);
|
var fileName = Path.GetFileName(filePath);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var wizardName = seedFile.Wizard.WizardName?.Trim();
|
var wizard = seedFile.Wizard;
|
||||||
if (string.IsNullOrWhiteSpace(wizardName))
|
if (string.IsNullOrWhiteSpace(wizard.WizardName))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("WizardName is empty, skipped: {FilePath}", filePath);
|
logger.LogWarning("WizardName is empty, skipped: {FilePath}", filePath);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isCustomComponent = seedFile.Wizard.ComponentKind == WizardComponentKindEnum.Custom;
|
var isCustomComponent = wizard.ComponentKind == WizardComponentKindEnum.Custom;
|
||||||
if (!isCustomComponent && seedFile.ListForm == null)
|
if (!isCustomComponent && seedFile.ListForm == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("[{File}] ListForm section is missing, skipped.", fileName);
|
logger.LogWarning("[{File}] ListForm section is missing, skipped.", fileName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await BackfillMenuParentModuleIdAsync(seedFile.Wizard);
|
if (seedFile.Pending != null)
|
||||||
|
|
||||||
// Zaten seeded mi kontrol et. Custom yolunda ListForm üretilmediği için
|
|
||||||
// idempotency menü kaydı üzerinden değerlendirilir.
|
|
||||||
// Menusuz Custom wizard'da ne ListForm ne de menu kaydi olustugu icin
|
|
||||||
// idempotency okuma permission'i uzerinden degerlendirilir.
|
|
||||||
var alreadySeeded = isCustomComponent
|
|
||||||
? seedFile.Wizard.CreateMenu
|
|
||||||
? await _repoMenu.AnyAsync(a => a.Code == seedFile.Wizard.ResolveMenuCode())
|
|
||||||
: await _repoPerm.AnyAsync(a => a.Name == seedFile.Wizard.ResolveMenuCode())
|
|
||||||
: await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode);
|
|
||||||
|
|
||||||
if (alreadySeeded)
|
|
||||||
{
|
{
|
||||||
// Ekran kurulu olsa da dil metinleri eksik kalmis olabilir: seed dosyasina yeni
|
logger.LogInformation("[{File}] '{WizardName}' has a pending deploy, previous records are being replaced...", fileName, wizard.WizardName);
|
||||||
// bir alan eklendiginde ya da ekran seeder disinda olustugunda sutun basliklari
|
await RemovePreviousRecordsAsync(seedFile);
|
||||||
// ham anahtar olarak gorunur. Mevcut metinlerin uzerine yazilmadigi icin bu
|
}
|
||||||
// tamamlama ekrani degistirmez.
|
else if (await installationChecker.IsInstalledAsync(isCustomComponent, wizard.ResolveMenuCode(), wizard.ResolveListFormCode()))
|
||||||
await SeedLanguageTextsAsync(seedFile.Wizard, isCustomComponent);
|
{
|
||||||
_logger.LogInformation("[{File}] '{WizardName}' already exists, only missing language texts were created.", fileName, wizardName);
|
logger.LogInformation("[{File}] '{WizardName}' already installed, skipped.", fileName, wizard.WizardName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("[{File}] '{WizardName}' is being applied...", fileName, wizardName);
|
await InstallAsync(seedFile);
|
||||||
await ApplyWizardSeedAsync(seedFile);
|
|
||||||
_logger.LogInformation("[{File}] '{WizardName}' has been successfully applied.", fileName, wizardName);
|
if (seedFile.Pending != null)
|
||||||
|
{
|
||||||
|
seedFile.Pending = null;
|
||||||
|
await File.WriteAllTextAsync(filePath, seedFile.ToSeedJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("[{File}] '{WizardName}' has been successfully applied.", fileName, wizard.WizardName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, $"Error - {filePath}: {ex.Message}");
|
logger.LogError(ex, "Error - {FilePath}: {Message}", filePath, ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyWizardSeedAsync(WizardSeedFileDto seedFile)
|
/// <summary>
|
||||||
|
/// Ekrana ozgu kayitlari kurar: veri kaynagi (yoksa), ListForm, alanlar ve is akisi dosyadaki
|
||||||
|
/// sozlesmeden. Menu/yetki/dil satirlari <c>ListFormSeedDataApplier</c>'a kalir.
|
||||||
|
/// </summary>
|
||||||
|
private async Task InstallAsync(WizardSeedFileDto seedFile)
|
||||||
{
|
{
|
||||||
var input = seedFile.Wizard;
|
var input = seedFile.Wizard;
|
||||||
input.Groups ??= [];
|
|
||||||
|
|
||||||
var code = input.ResolveMenuCode();
|
|
||||||
var listFormCode = input.ResolveListFormCode();
|
|
||||||
var nameLangKey = code;
|
|
||||||
var permCreateName = $"{code}.Create";
|
|
||||||
var permUpdateName = $"{code}.Update";
|
|
||||||
var permDeleteName = $"{code}.Delete";
|
|
||||||
var permExportName = $"{code}.Export";
|
|
||||||
var permImportName = $"{code}.Import";
|
|
||||||
var permNoteName = $"{code}.Note";
|
|
||||||
|
|
||||||
// Yol ayrımı: Custom yolunda ListForm ekosistemi üretilmez (AppService.Create ile aynı davranış).
|
|
||||||
var isCustomComponent = input.ComponentKind == WizardComponentKindEnum.Custom;
|
|
||||||
var customRoutePath = NormalizeSeedCustomRoutePath(input);
|
|
||||||
|
|
||||||
// Dil - Language Keys
|
|
||||||
await SeedLanguageTextsAsync(input, isCustomComponent);
|
|
||||||
|
|
||||||
// Permission Group - gorunen adin dil anahtari SeedLanguageTextsAsync icinde uretilir.
|
|
||||||
var groupName = ResolvePermissionGroupName(input);
|
|
||||||
input.PermissionGroupName = groupName;
|
|
||||||
if (!await _repoPermGroup.AnyAsync(a => a.Name == groupName))
|
|
||||||
{
|
|
||||||
var group = new PermissionGroupDefinitionRecord(Guid.NewGuid(), groupName, groupName);
|
|
||||||
group.SetPermissionOrder((await _repoPermGroup.GetListAsync()).NextPermissionOrder());
|
|
||||||
await _repoPermGroup.InsertAsync(group, autoSave: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permissions - tek seferde mevcut permission'ları çek, sonra her birini kontrol et
|
|
||||||
var permQueryable = await _repoPerm.GetQueryableAsync();
|
|
||||||
var existingPerms = permQueryable.Where(a => a.GroupName == groupName).ToList();
|
|
||||||
|
|
||||||
// Yetkiler menuyle ayni modul filtresine tabidir; modul bos kalirsa rol ekraninda
|
|
||||||
// modul secimiyle filtrelenen listede gorunmez.
|
|
||||||
var permissionModules = input.MenuParentModuleId?.Trim();
|
|
||||||
|
|
||||||
var permRead = existingPerms.FirstOrDefault(a => a.Name == code);
|
|
||||||
permRead ??= await InsertPermissionAsync(groupName, code, null, nameLangKey, permissionModules,
|
|
||||||
existingPerms.Where(p => string.IsNullOrWhiteSpace(p.ParentName)).NextPermissionOrder());
|
|
||||||
|
|
||||||
// CRUD üçlüsü her iki yolda da üretilir: Custom component de Form
|
|
||||||
// üzerinden kayıt ekler, günceller ve siler (AppService.Create ile aynı davranış).
|
|
||||||
// Alt yetkilerin sirasi kardesler arasindadir ve bu dizilimi izler (Create = 1, ...).
|
|
||||||
if (!existingPerms.Any(a => a.Name == permCreateName))
|
|
||||||
await InsertPermissionAsync(groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, permissionModules, 1);
|
|
||||||
|
|
||||||
if (!existingPerms.Any(a => a.Name == permUpdateName))
|
|
||||||
await InsertPermissionAsync(groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, permissionModules, 2);
|
|
||||||
|
|
||||||
if (!existingPerms.Any(a => a.Name == permDeleteName))
|
|
||||||
await InsertPermissionAsync(groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, permissionModules, 3);
|
|
||||||
|
|
||||||
// Export/Import/Note yalnızca ListForm ekosisteminde karşılığı olan işlemlerdir.
|
|
||||||
if (!isCustomComponent)
|
|
||||||
{
|
|
||||||
if (!existingPerms.Any(a => a.Name == permExportName))
|
|
||||||
await InsertPermissionAsync(groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, permissionModules, 4);
|
|
||||||
|
|
||||||
if (!existingPerms.Any(a => a.Name == permImportName))
|
|
||||||
await InsertPermissionAsync(groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, permissionModules, 5);
|
|
||||||
|
|
||||||
if (!existingPerms.Any(a => a.Name == permNoteName))
|
|
||||||
await InsertPermissionAsync(groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, permissionModules, 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
// // Permission Grants - Admin role için, sadece eksik olanları ekle
|
|
||||||
// var existingGrants = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName);
|
|
||||||
// var existingGrantNames = new HashSet<string>(existingGrants.Select(g => g.Name));
|
|
||||||
|
|
||||||
// var grantsToInsert = new[]
|
|
||||||
// {
|
|
||||||
// permRead.Name, permCreate.Name, permUpdate.Name,
|
|
||||||
// permDelete.Name, permExport.Name, permImport.Name, permNote.Name
|
|
||||||
// }
|
|
||||||
// .Where(name => !existingGrantNames.Contains(name))
|
|
||||||
// .Select(name => new PermissionGrant(Guid.NewGuid(), name, "R", PlatformConsts.AbpIdentity.User.AdminRoleName))
|
|
||||||
// .ToList();
|
|
||||||
|
|
||||||
// if (grantsToInsert.Count > 0)
|
|
||||||
// {
|
|
||||||
// await _permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: true);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Menunun acacagi adres; menu uretilmese de seed dosyasindaki degeri korunur.
|
|
||||||
input.MenuUrl = isCustomComponent
|
|
||||||
? customRoutePath
|
|
||||||
: !string.IsNullOrWhiteSpace(input.MenuUrl)
|
|
||||||
? input.MenuUrl.Trim()
|
|
||||||
: WizardConsts.MenuUrl(code);
|
|
||||||
|
|
||||||
// Menusuz wizard: menu (ve ust menu) kaydi hic uretilmez.
|
|
||||||
if (input.CreateMenu)
|
|
||||||
{
|
|
||||||
// Menu Parent
|
|
||||||
await EnsureModuleAsync(input.MenuParentModuleId);
|
|
||||||
|
|
||||||
var menuQueryable = await _repoMenu.GetQueryableAsync();
|
|
||||||
var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode);
|
|
||||||
if (menuParent == null)
|
|
||||||
{
|
|
||||||
var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0;
|
|
||||||
var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon)
|
|
||||||
? input.MenuParentIcon
|
|
||||||
: !string.IsNullOrWhiteSpace(input.MenuIcon)
|
|
||||||
? input.MenuIcon
|
|
||||||
: WizardConsts.MenuIcon;
|
|
||||||
menuParent = await _repoMenu.InsertAsync(new Menu
|
|
||||||
{
|
|
||||||
Code = input.MenuParentCode,
|
|
||||||
DisplayName = input.MenuParentCode,
|
|
||||||
IsDisabled = false,
|
|
||||||
ModuleId = input.MenuParentModuleId,
|
|
||||||
Icon = menuParentIcon,
|
|
||||||
Order = maxRootOrder + 1,
|
|
||||||
}, autoSave: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Menu
|
|
||||||
var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0;
|
|
||||||
var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1;
|
|
||||||
input.MenuOrder = menuOrder;
|
|
||||||
var menuUrl = input.MenuUrl;
|
|
||||||
var existingMenu = await _repoMenu.FirstOrDefaultAsync(a => a.Code == code);
|
|
||||||
if (existingMenu == null)
|
|
||||||
{
|
|
||||||
await _repoMenu.InsertAsync(new Menu
|
|
||||||
{
|
|
||||||
Code = code,
|
|
||||||
DisplayName = nameLangKey,
|
|
||||||
IsDisabled = false,
|
|
||||||
ParentCode = menuParent.Code,
|
|
||||||
Icon = input.MenuIcon ?? WizardConsts.MenuIcon,
|
|
||||||
Target = null,
|
|
||||||
ElementId = null,
|
|
||||||
CssClass = null,
|
|
||||||
Url = menuUrl,
|
|
||||||
RequiredPermissionName = permRead.Name,
|
|
||||||
Order = menuOrder,
|
|
||||||
ModuleId = menuParent.ModuleId,
|
|
||||||
}, autoSave: true);
|
|
||||||
}
|
|
||||||
else if (existingMenu.Order != menuOrder || existingMenu.Url != menuUrl)
|
|
||||||
{
|
|
||||||
existingMenu.Order = menuOrder;
|
|
||||||
existingMenu.Url = menuUrl;
|
|
||||||
await _repoMenu.UpdateAsync(existingMenu, autoSave: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom yolunda ListForm ekosistemi üretilmez.
|
// Custom yolunda ListForm ekosistemi üretilmez.
|
||||||
if (isCustomComponent)
|
if (input.ComponentKind == WizardComponentKindEnum.Custom)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// DataSource
|
var listFormCode = input.ResolveListFormCode();
|
||||||
var existingDataSource = await _repoDataSource.FirstOrDefaultAsync(a => a.Code == input.DataSourceCode);
|
|
||||||
if (existingDataSource == null)
|
if (!await repoDataSource.AnyAsync(a => a.Code == input.DataSourceCode))
|
||||||
{
|
{
|
||||||
await _repoDataSource.InsertAsync(new DataSource
|
await repoDataSource.InsertAsync(new DataSource
|
||||||
{
|
{
|
||||||
Code = input.DataSourceCode,
|
Code = input.DataSourceCode,
|
||||||
DataSourceType = input.DataSourceConnectionString != null &&
|
DataSourceType = input.DataSourceConnectionString?.Contains("Server", StringComparison.OrdinalIgnoreCase) == true
|
||||||
input.DataSourceConnectionString.IndexOf("Server", StringComparison.OrdinalIgnoreCase) >= 0
|
|
||||||
? DataSourceTypeEnum.Mssql
|
? DataSourceTypeEnum.Mssql
|
||||||
: DataSourceTypeEnum.Postgresql,
|
: DataSourceTypeEnum.Postgresql,
|
||||||
ConnectionString = input.DataSourceConnectionString
|
ConnectionString = input.DataSourceConnectionString
|
||||||
}, autoSave: true);
|
}, autoSave: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListForm ve alanlari - varsa sil, seed dosyasindaki sozlesmeden yeniden kur
|
await DeleteListFormAsync(listFormCode);
|
||||||
var existingListForm = await _repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == input.ListFormCode);
|
|
||||||
if (existingListForm != null)
|
|
||||||
{
|
|
||||||
await _repoListForm.DeleteAsync(existingListForm, autoSave: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingListFormFields = await _repoListFormField.GetListAsync(a => a.ListFormCode == input.ListFormCode);
|
|
||||||
if (existingListFormFields.Count > 0)
|
|
||||||
{
|
|
||||||
await _repoListFormField.DeleteManyAsync(existingListFormFields, autoSave: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingWorkflowCriteria = await _repoListFormWorkflow.GetListAsync(a => a.ListFormCode == input.ListFormCode);
|
|
||||||
if (existingWorkflowCriteria.Count > 0)
|
|
||||||
{
|
|
||||||
await _repoListFormWorkflow.DeleteManyAsync(existingWorkflowCriteria, autoSave: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ekran, seed dosyasinin tasidigi ListForm sozlesmesinden kurulur; boylece wizard
|
// Ekran, seed dosyasinin tasidigi ListForm sozlesmesinden kurulur; boylece wizard
|
||||||
// sonrasinda tasarimci uzerinde yapilan degisiklikler de geri gelir.
|
// sonrasinda tasarimci uzerinde yapilan degisiklikler de geri gelir.
|
||||||
var listForm = new ListForm();
|
var listForm = new ListForm();
|
||||||
seedFile.ListForm.ApplyTo(listForm);
|
seedFile.ListForm.ApplyTo(listForm);
|
||||||
await _repoListForm.InsertAsync(listForm, autoSave: true);
|
await repoListForm.InsertAsync(listForm, autoSave: true);
|
||||||
|
|
||||||
foreach (var fieldDto in seedFile.Fields)
|
foreach (var fieldDto in seedFile.Fields)
|
||||||
{
|
{
|
||||||
var field = new ListFormField();
|
var field = new ListFormField();
|
||||||
fieldDto.ApplyTo(field);
|
fieldDto.ApplyTo(field);
|
||||||
await _repoListFormField.InsertAsync(field, autoSave: true);
|
await repoListFormField.InsertAsync(field, autoSave: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var criteria in seedFile.ListForm.WorkflowDto.Criteria)
|
foreach (var criteria in seedFile.ListForm.WorkflowDto.Criteria)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(criteria.Id))
|
if (string.IsNullOrWhiteSpace(criteria.Id))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Workflow criteria skipped because Id is empty. ListFormCode: {ListFormCode}, Title: {Title}", input.ListFormCode, criteria.Title);
|
logger.LogWarning("Workflow criteria skipped because Id is empty. ListFormCode: {ListFormCode}, Title: {Title}", listFormCode, criteria.Title);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _repoListFormWorkflow.InsertAsync(new ListFormWorkflow(criteria.Id)
|
await repoListFormWorkflow.InsertAsync(new ListFormWorkflow(criteria.Id)
|
||||||
{
|
{
|
||||||
ListFormCode = string.IsNullOrWhiteSpace(criteria.ListFormCode) ? input.ListFormCode : criteria.ListFormCode,
|
ListFormCode = string.IsNullOrWhiteSpace(criteria.ListFormCode) ? listFormCode : criteria.ListFormCode,
|
||||||
Kind = criteria.Kind,
|
Kind = criteria.Kind,
|
||||||
Title = criteria.Title,
|
Title = criteria.Title,
|
||||||
CompareColumn = criteria.CompareColumn,
|
CompareColumn = criteria.CompareColumn,
|
||||||
|
|
@ -404,160 +212,89 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Custom yolunda menunun acacagi rotayi normalize eder. Rota MenuUrl uzerinden
|
/// Bekleyen deploy'dan once wizard'in veritabanindaki kayitlarini kaldirir: onceki surumlerin
|
||||||
/// tasinir; bos ise menu hicbir yere gitmeyecegi icin seed dosyasi hatali kabul edilir.
|
/// (<see cref="WizardPendingDto.Replaced"/>) ve guncel surumun sahip oldugu menu, yetki, izin grubu
|
||||||
|
/// ve dil anahtarlari ile onceki ekran kodlari. Guncel satirlar dosyada durdugu icin applier onlari
|
||||||
|
/// yeni degerleriyle geri ekler; dosyada artik olmayanlar kalici olarak gider. Boylece migrate'in
|
||||||
|
/// "yalnizca eksik olani ekle" kurali duzenlenmis bir ekrani da guncel haline getirir.
|
||||||
|
/// <para>
|
||||||
|
/// Kaplar korunur: baska bir kaydin hala bagli oldugu ust menu ve izin grubu silinmez. Modul ve veri
|
||||||
|
/// kaynagi kaldirilmaz; ikisi de baska ekranlarca paylasilabilir ve kalmalari zararsizdir. Rollerin
|
||||||
|
/// yetki atamalari yetki adina baglidir; yetki geri eklendiginde atamalar yerindedir, yalnizca
|
||||||
|
/// kalici olarak giden yetkinin atamalari silinir.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string NormalizeSeedCustomRoutePath(ListFormWizardDto input)
|
private async Task RemovePreviousRecordsAsync(WizardSeedFileDto seedFile)
|
||||||
{
|
{
|
||||||
if (input.ComponentKind != WizardComponentKindEnum.Custom)
|
var pending = seedFile.Pending!;
|
||||||
return null;
|
var owned = (seedFile.InsertedRecords ?? new WizardInsertedRecordsDto()).Union(pending.Replaced);
|
||||||
|
var currentPermissions = WizardSeedRowKeys.PermissionNames(seedFile.Wizard).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var routePath = input.MenuUrl?.Trim();
|
foreach (var listFormCode in pending.ListFormCodes ?? [])
|
||||||
if (string.IsNullOrWhiteSpace(routePath))
|
await DeleteListFormAsync(listFormCode);
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"'{input.WizardName}' custom component wizard'inda Route Path bos olamaz.");
|
|
||||||
|
|
||||||
return routePath.StartsWith('/') ? routePath : "/" + routePath;
|
var menuCodes = owned.MenuCodes.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
var menus = await repoMenu.GetListAsync(a => menuCodes.Contains(a.Code));
|
||||||
|
foreach (var menu in menus)
|
||||||
/// <summary>
|
|
||||||
/// Seed dosyasindaki modul kaydi yoksa olusturur; menunun ModuleId yabanci anahtari
|
|
||||||
/// tanimsiz bir module bagli kalmasin diye menu yazilmadan once cagrilir.
|
|
||||||
/// </summary>
|
|
||||||
private async Task EnsureModuleAsync(string moduleId)
|
|
||||||
{
|
|
||||||
var cleaned = moduleId?.Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(cleaned))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (await _repoModule.AnyAsync(a => a.Id == cleaned))
|
|
||||||
return;
|
|
||||||
|
|
||||||
await _repoModule.InsertAsync(new Module(cleaned) { Name = cleaned }, autoSave: true);
|
|
||||||
_logger.LogInformation("Module '{ModuleId}' created.", cleaned);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<PermissionDefinitionRecord> InsertPermissionAsync(
|
|
||||||
string groupName,
|
|
||||||
string name,
|
|
||||||
string parentName,
|
|
||||||
string displayName,
|
|
||||||
string modules,
|
|
||||||
int order)
|
|
||||||
{
|
|
||||||
var permission = new PermissionDefinitionRecord(
|
|
||||||
Guid.NewGuid(), groupName, name, parentName, displayName, true, MultiTenancySides.Both);
|
|
||||||
permission.SetModules(modules);
|
|
||||||
permission.SetPermissionOrder(order);
|
|
||||||
|
|
||||||
return await _repoPerm.InsertAsync(permission, autoSave: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task BackfillMenuParentModuleIdAsync(ListFormWizardDto input)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(input.MenuParentCode) ||
|
|
||||||
string.IsNullOrWhiteSpace(input.MenuParentModuleId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
await EnsureModuleAsync(input.MenuParentModuleId);
|
|
||||||
|
|
||||||
var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode);
|
|
||||||
if (menuParent == null || !string.IsNullOrWhiteSpace(menuParent.ModuleId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
menuParent.ModuleId = input.MenuParentModuleId.Trim();
|
|
||||||
await _repoMenu.UpdateAsync(menuParent, autoSave: true);
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Menu parent '{MenuParentCode}' ModuleId backfilled as '{ModuleId}'.",
|
|
||||||
input.MenuParentCode,
|
|
||||||
menuParent.ModuleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Wizard'in urettigi ekranin dil metinleri: menu adi, ust menu adi, izin grubu gorunen adi,
|
|
||||||
/// ekran basligi/aciklamasi ve alan basliklari. Ekran kurulumundan bagimsiz calisir; boylece
|
|
||||||
/// zaten kurulu bir ekranda eksik kalan anahtarlar da her seed calismasinda tamamlanir.
|
|
||||||
/// </summary>
|
|
||||||
private async Task SeedLanguageTextsAsync(ListFormWizardDto input, bool isCustomComponent)
|
|
||||||
{
|
|
||||||
await CreateLangKeyAsync(input.ResolveMenuCode(), input.MenuEn, input.MenuTr);
|
|
||||||
|
|
||||||
// Ust menu ve izin grubu paylasilan kayitlardir: ilk wizard onlari uretmis olsa bile dil
|
|
||||||
// metinleri eksik kalabilir, bu yuzden menu/grup kaydinin varligindan bagimsiz yazilir.
|
|
||||||
if (input.CreateMenu)
|
|
||||||
await CreateLangKeyAsync(input.MenuParentCode, input.MenuParentEn, input.MenuParentTr);
|
|
||||||
|
|
||||||
var groupName = ResolvePermissionGroupName(input);
|
|
||||||
|
|
||||||
// Grup adi ust menu koduyla ayniysa anahtar tektir; menu adi ile izin grubu adi ayni
|
|
||||||
// metni gosterir, ayrica gorunen ad girilmediyse menu metinlerine duser.
|
|
||||||
if (!string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
await CreateLangKeyAsync(groupName, input.PermissionGroupEn?.Trim(), input.PermissionGroupTr?.Trim());
|
// Wizard disindan bir alt menusu olan ust menu kalir; aksi halde alt menu sahipsiz kalirdi.
|
||||||
}
|
if (await repoMenu.AnyAsync(a => a.ParentCode == menu.Code && !menuCodes.Contains(a.Code)))
|
||||||
else
|
continue;
|
||||||
{
|
|
||||||
await CreateLangKeyAsync(groupName,
|
await repoMenu.DeleteAsync(menu, autoSave: true);
|
||||||
input.PermissionGroupEn?.Trim() ?? input.MenuParentEn,
|
|
||||||
input.PermissionGroupTr?.Trim() ?? input.MenuParentTr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom yolunda ListForm ekosistemi (baslik, aciklama, alan basliklari) uretilmez.
|
var permissionNames = owned.PermissionNames.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
if (isCustomComponent)
|
var permissions = await repoPerm.GetListAsync(a => permissionNames.Contains(a.Name));
|
||||||
return;
|
if (permissions.Count > 0)
|
||||||
|
await repoPerm.DeleteManyAsync(permissions, autoSave: true);
|
||||||
|
|
||||||
var listFormCode = input.ResolveListFormCode();
|
foreach (var name in permissionNames.Where(n => !currentPermissions.Contains(n)))
|
||||||
await CreateLangKeyAsync($"{listFormCode}.Title", input.TitleEn, input.TitleTr);
|
|
||||||
await CreateLangKeyAsync($"{listFormCode}.Desc", input.DescEn, input.DescTr);
|
|
||||||
|
|
||||||
// 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);
|
var grants = await repoPermGrant.GetListAsync(a => a.Name == name);
|
||||||
|
if (grants.Count > 0)
|
||||||
|
await repoPermGrant.DeleteManyAsync(grants, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var groupName in owned.PermissionGroupNames)
|
||||||
|
{
|
||||||
|
if (await repoPerm.AnyAsync(a => a.GroupName == groupName))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var group = await repoPermGroup.FirstOrDefaultAsync(a => a.Name == groupName);
|
||||||
|
if (group != null)
|
||||||
|
await repoPermGroup.DeleteAsync(group, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var key in owned.LanguageKeys)
|
||||||
|
{
|
||||||
|
var texts = await repoLangText.GetListAsync(a => a.ResourceName == _appName && a.Key == key);
|
||||||
|
if (texts.Count > 0)
|
||||||
|
await repoLangText.DeleteManyAsync(texts, autoSave: true);
|
||||||
|
|
||||||
|
var languageKey = await repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == _appName && a.Key == key);
|
||||||
|
if (languageKey != null)
|
||||||
|
await repoLangKey.DeleteAsync(languageKey, autoSave: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Yetkilerin toplanacagi izin grubu adi. Menuye eklenmeyen wizard'da grup sorulmaz; bos
|
/// Ekrani, alanlarini ve is akisini siler. Alanlar ve is akisi ListForm'a zorunlu (cascade)
|
||||||
/// geldiginde varsayilan uygulama grubuna duser.
|
/// anahtarla baglidir; ekran once silinirse EF izlenen bagimlilari da siler ve ardindan gelen
|
||||||
|
/// toplu silme concurrency hatasi firlatir. Bu yuzden bagimlilar once silinir.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string ResolvePermissionGroupName(ListFormWizardDto input) =>
|
private async Task DeleteListFormAsync(string listFormCode)
|
||||||
string.IsNullOrWhiteSpace(input.PermissionGroupName) ? AppName : input.PermissionGroupName.Trim();
|
|
||||||
|
|
||||||
private async Task CreateLangKeyAsync(string key, string textEn, string textTr)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(key)) return;
|
var fields = await repoListFormField.GetListAsync(a => a.ListFormCode == listFormCode);
|
||||||
|
if (fields.Count > 0)
|
||||||
|
await repoListFormField.DeleteManyAsync(fields, autoSave: true);
|
||||||
|
|
||||||
var langKey = await _repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == _appName && a.Key == key)
|
var criteria = await repoListFormWorkflow.GetListAsync(a => a.ListFormCode == listFormCode);
|
||||||
?? await _repoLangKey.InsertAsync(new LanguageKey { ResourceName = _appName, Key = key }, autoSave: true);
|
if (criteria.Count > 0)
|
||||||
|
await repoListFormWorkflow.DeleteManyAsync(criteria, autoSave: true);
|
||||||
|
|
||||||
var existingTexts = await _repoLangText.GetListAsync(a => a.ResourceName == _appName && a.Key == langKey.Key);
|
var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
||||||
|
if (listForm != null)
|
||||||
var existingEn = existingTexts.FirstOrDefault(a => a.CultureName == _cultureNameDefault);
|
await repoListForm.DeleteAsync(listForm, autoSave: true);
|
||||||
if (existingEn == null)
|
|
||||||
{
|
|
||||||
await _repoLangText.InsertAsync(new LanguageText
|
|
||||||
{
|
|
||||||
ResourceName = _appName,
|
|
||||||
Key = langKey.Key,
|
|
||||||
CultureName = _cultureNameDefault,
|
|
||||||
Value = textEn ?? key
|
|
||||||
}, autoSave: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingTr = existingTexts.FirstOrDefault(a => a.CultureName == LanguageCodes.Tr);
|
|
||||||
if (existingTr == null)
|
|
||||||
{
|
|
||||||
await _repoLangText.InsertAsync(new LanguageText
|
|
||||||
{
|
|
||||||
ResourceName = _appName,
|
|
||||||
Key = langKey.Key,
|
|
||||||
CultureName = LanguageCodes.Tr,
|
|
||||||
Value = textTr ?? key
|
|
||||||
}, autoSave: true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace Sozsoft.Platform.Enums;
|
namespace Sozsoft.Platform.Enums;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wizard silme raporundaki kayit turleri. Ilk grup wizard'in urettigi kayitlardir; ikinci grup
|
/// Wizard silme raporundaki kayit turleri. Ilk grup wizard'in urettigi kayitlardir; ikinci grup
|
||||||
|
|
@ -37,7 +37,7 @@ public enum WizardDeleteStatusEnum
|
||||||
{
|
{
|
||||||
Deleted = 0,
|
Deleted = 0,
|
||||||
|
|
||||||
/// <summary>Kayit baska bir yerde kullanildigi ya da yeniden deploy edilecegi icin birakildi.</summary>
|
/// <summary>Kayit baska bir yerde kullanildigi icin birakildi.</summary>
|
||||||
Kept = 1,
|
Kept = 1,
|
||||||
|
|
||||||
/// <summary>Seed dosyasinda izi var ama veritabaninda yok; daha once elle silinmis olabilir.</summary>
|
/// <summary>Seed dosyasinda izi var ama veritabaninda yok; daha once elle silinmis olabilir.</summary>
|
||||||
|
|
@ -52,9 +52,6 @@ public enum WizardDeleteReasonEnum
|
||||||
/// <summary>Baska bir kayit bu kayda baglidir; kullananlar raporda listelenir.</summary>
|
/// <summary>Baska bir kayit bu kayda baglidir; kullananlar raporda listelenir.</summary>
|
||||||
InUse = 1,
|
InUse = 1,
|
||||||
|
|
||||||
/// <summary>Duzenleme yolunda paylasilan kayit korunur; deploy onu yeniden kullanir.</summary>
|
|
||||||
Redeploy = 2,
|
|
||||||
|
|
||||||
/// <summary>Tablo silme onayinda secilmedi; tablo ve ona bagli seed dosyalari yerinde kalir.</summary>
|
/// <summary>Tablo silme onayinda secilmedi; tablo ve ona bagli seed dosyalari yerinde kalir.</summary>
|
||||||
NotSelected = 3,
|
NotSelected = 3,
|
||||||
|
|
||||||
|
|
|
||||||
29
api/src/Sozsoft.Platform.Domain.Shared/SeedJsonText.cs
Normal file
29
api/src/Sozsoft.Platform.Domain.Shared/SeedJsonText.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seed dosyalarina yazilan JSON metninin son hali. <c>UnsafeRelaxedJsonEscaping</c> Turkce harfleri
|
||||||
|
/// ve BMP'deki karakterleri oldugu gibi birakir ama BMP disindaki karakterleri (emoji) her zaman
|
||||||
|
/// <c>🔥</c> bicimine kacirir. Dosya bastan yazildiginda elle girilmis her emoji bu bicime
|
||||||
|
/// doner ve diff'te sahte fark uretir; bu yuzden vekil ciftleri gercek karakterlerine geri cevrilir.
|
||||||
|
/// </summary>
|
||||||
|
public static partial class SeedJsonText
|
||||||
|
{
|
||||||
|
// Onunde tek sayida ters bolu varsa kacis gercektir; cift sayidaysa metnin kendisidir ("\\uD83D").
|
||||||
|
[GeneratedRegex(@"(?<!\\)((?:\\\\)*)\\u(D[89AB][0-9A-F]{2})\\u(D[C-F][0-9A-F]{2})", RegexOptions.IgnoreCase)]
|
||||||
|
private static partial Regex EscapedSurrogatePair();
|
||||||
|
|
||||||
|
public static string RestoreSurrogatePairs(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(json) || !json.Contains("\\u", StringComparison.Ordinal))
|
||||||
|
return json;
|
||||||
|
|
||||||
|
return EscapedSurrogatePair().Replace(json, m =>
|
||||||
|
m.Groups[1].Value
|
||||||
|
+ (char)int.Parse(m.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture)
|
||||||
|
+ (char)int.Parse(m.Groups[3].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,16 +12,7 @@ public static class WizardConsts
|
||||||
public static int EditFormHeightByRow => 75;
|
public static int EditFormHeightByRow => 75;
|
||||||
|
|
||||||
public static string WizardKey(string code) => $"{Prefix.App}.Wizard.{code}";
|
public static string WizardKey(string code) => $"{Prefix.App}.Wizard.{code}";
|
||||||
public static string WizardKeyTitle(string code) => $"{WizardKey(code)}.Title";
|
|
||||||
public static string WizardKeyDesc(string code) => $"{WizardKey(code)}.Desc";
|
|
||||||
public static string WizardKeyParent(string code) => $"{WizardKey(code)}.Parent";
|
|
||||||
|
|
||||||
public static string PermCreate(string code) => $"{WizardKey(code)}.Create";
|
|
||||||
public static string PermUpdate(string code) => $"{WizardKey(code)}.Update";
|
|
||||||
public static string PermDelete(string code) => $"{WizardKey(code)}.Delete";
|
|
||||||
public static string PermExport(string code) => $"{WizardKey(code)}.Export";
|
|
||||||
public static string PermImport(string code) => $"{WizardKey(code)}.Import";
|
|
||||||
public static string PermNote(string code) => $"{WizardKey(code)}.Note";
|
|
||||||
public static string LangKeyCreate => "App.Platform.Create";
|
public static string LangKeyCreate => "App.Platform.Create";
|
||||||
public static string LangKeyUpdate => "App.Platform.Update";
|
public static string LangKeyUpdate => "App.Platform.Update";
|
||||||
public static string LangKeyDelete => "App.Platform.Delete";
|
public static string LangKeyDelete => "App.Platform.Delete";
|
||||||
|
|
|
||||||
|
|
@ -42,4 +42,12 @@ public class ListFormSeedDataFileDto
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dosyaya yazilacak metin. Seed dosyasi yazan her yol bunu kullanir; emoji gibi karakterler
|
||||||
|
/// kacirilmadan yazilir (<see cref="SeedJsonText"/>) ve dosya depodaki digerleri gibi satir sonuyla
|
||||||
|
/// biter — dosya bastan yazildiginda sahte fark olusmaz.
|
||||||
|
/// </summary>
|
||||||
|
public static string Serialize(ListFormSeedDataFileDto file) =>
|
||||||
|
SeedJsonText.RestoreSurrogatePairs(JsonSerializer.Serialize(file, JsonOptions)) + "\n";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,7 @@ public class PlatformDbMigrationService : ITransientDependency
|
||||||
var seed = _configuration.GetValue("Seed", false);
|
var seed = _configuration.GetValue("Seed", false);
|
||||||
Logger.LogInformation($"Migrating database. Seed: {seed}. Env: {Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")}");
|
Logger.LogInformation($"Migrating database. Seed: {seed}. Env: {Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")}");
|
||||||
|
|
||||||
//Clear Cache
|
ClearCache();
|
||||||
var redis = ConnectionMultiplexer.Connect(_configuration["Redis:Configuration"]!);
|
|
||||||
var db = redis.GetDatabase();
|
|
||||||
db.Execute("FLUSHALL");
|
|
||||||
|
|
||||||
var initialMigrationAdded = AddInitialMigrationIfNotExist();
|
var initialMigrationAdded = AddInitialMigrationIfNotExist();
|
||||||
|
|
||||||
|
|
@ -110,9 +107,20 @@ public class PlatformDbMigrationService : ITransientDependency
|
||||||
|
|
||||||
await ExecuteFinalSeedersAsync(tenants);
|
await ExecuteFinalSeedersAsync(tenants);
|
||||||
|
|
||||||
|
// Seed; menu, yetki ve dil metni ekler. Calisan uygulama bu arada eski halleri onbellege
|
||||||
|
// almis olabilir (ornegin wizard'in yeni menusu); migrate sonrasinda da temizlenir.
|
||||||
|
if (seed)
|
||||||
|
ClearCache();
|
||||||
|
|
||||||
Logger.LogInformation("You can safely end this process...");
|
Logger.LogInformation("You can safely end this process...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ClearCache()
|
||||||
|
{
|
||||||
|
using var redis = ConnectionMultiplexer.Connect(_configuration["Redis:Configuration"]!);
|
||||||
|
redis.GetDatabase().Execute("FLUSHALL");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tüm host ve tenant migration/seed adımları bittikten sonra,
|
/// Tüm host ve tenant migration/seed adımları bittikten sonra,
|
||||||
/// {host|tenants/{tenantId}}/{sql|postgres}/execute klasöründeki script'lerin dosya adından
|
/// {host|tenants/{tenantId}}/{sql|postgres}/execute klasöründeki script'lerin dosya adından
|
||||||
|
|
|
||||||
42
api/src/Sozsoft.Platform.Domain/Data/SeedRowIds.cs
Normal file
42
api/src/Sozsoft.Platform.Domain/Data/SeedRowIds.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Data.Seeds;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seed dosyasina yazilan <c>Guid</c> anahtarli satirlarin kimligi. Kimlik is anahtarindan
|
||||||
|
/// turetilir (URL isim alanli, RFC 4122 surum 5); ayni satir her ortamda ve her uretimde ayni
|
||||||
|
/// kimligi alir, dosyada sahte fark ya da kopya satir olusmaz.
|
||||||
|
/// </summary>
|
||||||
|
public static class SeedRowIds
|
||||||
|
{
|
||||||
|
/// <summary>RFC 4122 URL isim alani (<c>6ba7b811-9dad-11d1-80b4-00c04fd430c8</c>).</summary>
|
||||||
|
private static readonly byte[] UrlNamespace =
|
||||||
|
[
|
||||||
|
0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dil metni satirinin kimligi: <c>{Tablo}:{ResourceName}:{Key}:{CultureName}</c> adindan uretilir.
|
||||||
|
/// </summary>
|
||||||
|
public static Guid LanguageText(string resourceName, string key, string cultureName) =>
|
||||||
|
NameBased($"{TableNameResolver.GetFullTableName(nameof(TableNameEnum.LanguageText))}:{resourceName}:{key}:{cultureName}");
|
||||||
|
|
||||||
|
private static Guid NameBased(string name)
|
||||||
|
{
|
||||||
|
var nameBytes = Encoding.UTF8.GetBytes(name);
|
||||||
|
var input = new byte[UrlNamespace.Length + nameBytes.Length];
|
||||||
|
UrlNamespace.CopyTo(input, 0);
|
||||||
|
nameBytes.CopyTo(input, UrlNamespace.Length);
|
||||||
|
|
||||||
|
var hash = SHA1.HashData(input);
|
||||||
|
|
||||||
|
// Surum (5) ve varyant (RFC 4122) bitleri.
|
||||||
|
hash[6] = (byte)((hash[6] & 0x0F) | 0x50);
|
||||||
|
hash[8] = (byte)((hash[8] & 0x3F) | 0x80);
|
||||||
|
|
||||||
|
return new Guid(hash.AsSpan(0, 16), bigEndian: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Platform.Entities;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
using Volo.Abp.PermissionManagement;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Data.Seeds;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir wizard ekraninin bu veritabaninda kurulu olup olmadigi: List yolunda ListForm kaydi, Custom
|
||||||
|
/// yolunda (ListForm uretilmedigi icin) okuma yetkisi. Migrate'in "kur ya da atla" karari
|
||||||
|
/// (<c>WizardDataSeeder</c>) ile Wizard Manager'daki "migrate bekliyor" durumu ayni kuraldir; tek
|
||||||
|
/// yerde durur.
|
||||||
|
/// </summary>
|
||||||
|
public class WizardInstallationChecker(
|
||||||
|
IRepository<ListForm, Guid> repoListForm,
|
||||||
|
IRepository<PermissionDefinitionRecord, Guid> repoPerm) : ITransientDependency
|
||||||
|
{
|
||||||
|
public async Task<bool> IsInstalledAsync(bool isCustom, string menuCode, string listFormCode) =>
|
||||||
|
isCustom
|
||||||
|
? await repoPerm.AnyAsync(a => a.Name == menuCode)
|
||||||
|
: await repoListForm.AnyAsync(a => a.ListFormCode == listFormCode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Birden fazla wizard icin ayni karar; iki sorguyla verilir. Anahtar, List yolunda ekran kodu,
|
||||||
|
/// Custom yolunda menu kodudur (buyuk/kucuk harf duyarsiz).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<HashSet<string>> GetInstalledAsync(IReadOnlyCollection<string> listFormCodes, IReadOnlyCollection<string> menuCodes)
|
||||||
|
{
|
||||||
|
var installed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (listFormCodes.Count > 0)
|
||||||
|
installed.UnionWith((await repoListForm.GetListAsync(a => listFormCodes.Contains(a.ListFormCode))).Select(a => a.ListFormCode));
|
||||||
|
|
||||||
|
if (menuCodes.Count > 0)
|
||||||
|
installed.UnionWith((await repoPerm.GetListAsync(a => menuCodes.Contains(a.Name))).Select(a => a.Name));
|
||||||
|
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
claude.md
28
claude.md
|
|
@ -228,9 +228,21 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
Hazır prompt'lar (`McpPromptNames` + `McpDevelopPrompts`) yalnızca metindir; adımları tekrar
|
Hazır prompt'lar (`McpPromptNames` + `McpDevelopPrompts`) yalnızca metindir; adımları tekrar
|
||||||
etmez, `get_dev_guide` bölümüne yönlendirir. Bir kural bağlayıcıysa prompt'a değil araca yazılır
|
etmez, `get_dev_guide` bölümüne yönlendirir. Bir kural bağlayıcıysa prompt'a değil araca yazılır
|
||||||
(örnek: `Ready to Start` şartı `start_dev_request`'tedir).
|
(örnek: `Ready to Start` şartı `start_dev_request`'tedir).
|
||||||
|
- **Wizard deploy veritabanına yazmaz** → wizard ekranı, MCP `deploy_wizard` ve düzenleme aynı
|
||||||
|
yoldan (`ListFormWizardAppService.Create`) geçer ve **yalnızca seed dosyası yazar**: `wizard/{Ad}.json`
|
||||||
|
+ menü/modül/izin grubu/yetki/dil satırları (`data/App.Menus.{Menu,Module}.json`,
|
||||||
|
`data/Abp.Identity.Permission{Groups,s}.json`, `data/App.Languages.LanguageText.json`;
|
||||||
|
`WizardSeedRowStore`, anahtarlar `WizardSeedRowKeys`). Kurulum yalnızca DB Migrate'tedir:
|
||||||
|
`WizardDataSeeder` ekrana özgü kayıtları (veri kaynağı, ListForm, alan, iş akışı) kurar,
|
||||||
|
`ListFormSeedDataApplier` satırları ekler. Deploy/import dosyaya `Pending` yazar; Wizard Manager
|
||||||
|
onu "Migrate bekliyor" gösterir (`WizardFileInfoDto.IsPending`), migrate önceki sürümün kayıtlarını
|
||||||
|
(`Pending.Replaced`) kaldırıp ekranı yeniden kurar ve `Pending`'i siler. Yetkiler **hiçbir role
|
||||||
|
otomatik atanmaz** (admin dahil); rol ekranından verilir. Deploy'a veritabanı yazan ya da silen bir
|
||||||
|
adım ekleme. Ayrıntı: `lowcode.instructions.md` §4.0.
|
||||||
- **MCP ile demo veri** → geliştirme isteği örnek satır taşıyorsa (prompt'taki tablo/liste, ekran
|
- **MCP ile demo veri** → geliştirme isteği örnek satır taşıyorsa (prompt'taki tablo/liste, ekran
|
||||||
görüntüsü) ekran `deploy_wizard`'a `SeedData: true` ile gider (`SeedFilePath = data/{ListFormCode}.json`
|
görüntüsü) ekran `deploy_wizard`'a `SeedData: true` ile gider (`SeedFilePath = data/{ListFormCode}.json`
|
||||||
+ `SeedSync*` açık, `ListFormWizardDto.BuildListForm`), satırlar ardından `insert_row` ile eklenir ve
|
+ `SeedSync*` açık, `ListFormWizardDto.BuildListForm`); kullanıcı DB Migrate çalıştırıp ekran
|
||||||
|
kurulduktan sonra (`isPending: false`) satırlar `insert_row` ile eklenir ve
|
||||||
senkronizasyon onları `configs/seeds/host/data/{ListFormCode}.json` dosyasına yazar; migrate
|
senkronizasyon onları `configs/seeds/host/data/{ListFormCode}.json` dosyasına yazar; migrate
|
||||||
ekranla birlikte seed eder. Dosya elle yazılmaz, görselden okunamayan değer uydurulmaz.
|
ekranla birlikte seed eder. Dosya elle yazılmaz, görselden okunamayan değer uydurulmaz.
|
||||||
`SeedData` wizard ekranında sorulmaz ama düzenlemede korunur (`RefreshFrom` + UI modeli).
|
`SeedData` wizard ekranında sorulmaz ama düzenlemede korunur (`RefreshFrom` + UI modeli).
|
||||||
|
|
@ -321,10 +333,11 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
- **Wizard import'u** → analiz dosyalar yazılmadan **önce** doğrular (`WizardImportValidator`);
|
- **Wizard import'u** → analiz dosyalar yazılmadan **önce** doğrular (`WizardImportValidator`);
|
||||||
`Error` seviyesinde bir bulgu varsa oturuma `blocked` işareti düşer ve `ApplyImport` reddeder.
|
`Error` seviyesinde bir bulgu varsa oturuma `blocked` işareti düşer ve `ApplyImport` reddeder.
|
||||||
Kapanış (`CompleteImport`) bağımlılık sırasıyla çalışır: `sql/object` → `sql/execute` → `crud`
|
Kapanış (`CompleteImport`) bağımlılık sırasıyla çalışır: `sql/object` → `sql/execute` → `crud`
|
||||||
→ `data` → `wizard`. Tablo eksikse önce scripti çalışır; sıra değiştirilmez. Ekran kurulumu
|
→ `data` → `wizard`. Tablo eksikse önce scripti çalışır; sıra değiştirilmez. Ekran import'ta
|
||||||
wizard ekranıyla **aynı** yoldan geçer (`DeployWizardAsync`); dosyadaki `ListForm`/`Fields`
|
**kurulmaz**: wizard dosyasına `Pending` yazılır ve ekran deploy'daki gibi DB Migrate'te kurulur;
|
||||||
sözleşmesi olduğu gibi kullanılır, wizard cevaplarından yeniden türetilmez — türetmek tasarımcı
|
dosyadaki `ListForm`/`Fields` sözleşmesi olduğu gibi kullanılır, wizard cevaplarından yeniden
|
||||||
değişikliklerini silerdi. Var olan ekrana dokunulmaz, her ekran kendi UoW'unda kurulur.
|
türetilmez — türetmek tasarımcı değişikliklerini silerdi. Wizard'ın menü/yetki/dil satırlarını
|
||||||
|
taşıyan platform dosyaları `data` adımında uygulanmaz (`WizardSeedRowStore.ListFormCodes`).
|
||||||
Engelleyici bulgu iki sınıftır: **arşiv kaynaklı** (`RequiresReexport` — bozuk/eksik dosya,
|
Engelleyici bulgu iki sınıftır: **arşiv kaynaklı** (`RequiresReexport` — bozuk/eksik dosya,
|
||||||
karşılığı olmayan referans) hedef ortamda giderilemez, export tekrarlanır; **ortam kaynaklı**
|
karşılığı olmayan referans) hedef ortamda giderilemez, export tekrarlanır; **ortam kaynaklı**
|
||||||
(eksik tablo, şema farkı) ekrandan giderilir. Yeni bir `Error` eklerken hangisi olduğu işaretlenir.
|
(eksik tablo, şema farkı) ekrandan giderilir. Yeni bir `Error` eklerken hangisi olduğu işaretlenir.
|
||||||
|
|
@ -348,6 +361,11 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
eklenir, varsa dokunulmaz; import'ta `overwriteExisting` açıktır: var olan kayıt dosyadaki
|
eklenir, varsa dokunulmaz; import'ta `overwriteExisting` açıktır: var olan kayıt dosyadaki
|
||||||
değerlerle güncellenir ve soft delete izleri (`IsDeleted`/`DeletionTime`/`DeleterId`) temizlenir. Yeni bir kayıt yolu (yeni endpoint, toplu işlem, tipli AppService) eklenirse
|
değerlerle güncellenir ve soft delete izleri (`IsDeleted`/`DeletionTime`/`DeleterId`) temizlenir. Yeni bir kayıt yolu (yeni endpoint, toplu işlem, tipli AppService) eklenirse
|
||||||
senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar.
|
senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar.
|
||||||
|
- **Seed dosyası yazmak** → veri dosyası `ListFormSeedDataFileDto.Serialize`, wizard dosyası
|
||||||
|
`WizardSeedFileDto.ToSeedJson` ile yazılır; düz `JsonSerializer.Serialize` + `File.WriteAllText`
|
||||||
|
kullanılmaz. System.Text.Json emojiyi her zaman vekil çift kaçışına çevirir; dosya baştan
|
||||||
|
yazıldığında bütün emojiler sahte fark üretir. İkisi de `SeedJsonText.RestoreSurrogatePairs`'ten
|
||||||
|
geçer.
|
||||||
- **`crud/{Entity}.json`** → uygulama kuralı `CrudEndpointSeedApplier`'dadır ve damga
|
- **`crud/{Entity}.json`** → uygulama kuralı `CrudEndpointSeedApplier`'dadır ve damga
|
||||||
karşılaştırmalıdır: eksik endpoint eklenir, mevcut kayıt yalnızca dosyanın `GeneratedAt` değeri
|
karşılaştırmalıdır: eksik endpoint eklenir, mevcut kayıt yalnızca dosyanın `GeneratedAt` değeri
|
||||||
kaydın `LastModificationTime`/`CreationTime` değerinden yeniyse güncellenir/silinir. Migrate
|
kaydın `LastModificationTime`/`CreationTime` değerinden yeniyse güncellenir/silinir. Migrate
|
||||||
|
|
|
||||||
|
|
@ -38322,20 +38322,6 @@
|
||||||
"Key": "App.ListForm.WizardDeleteReason.InUse",
|
"Key": "App.ListForm.WizardDeleteReason.InUse",
|
||||||
"Value": "Başka bir yerde kullanılıyor"
|
"Value": "Başka bir yerde kullanılıyor"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"Id": "5e269bab-a213-52de-a06b-03ae3e4d106b",
|
|
||||||
"CultureName": "en",
|
|
||||||
"ResourceName": "Platform",
|
|
||||||
"Key": "App.ListForm.WizardDeleteReason.Redeploy",
|
|
||||||
"Value": "Kept for redeploy"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Id": "6cb9c831-1b8d-55f0-9827-3b55e02adb6b",
|
|
||||||
"CultureName": "tr",
|
|
||||||
"ResourceName": "Platform",
|
|
||||||
"Key": "App.ListForm.WizardDeleteReason.Redeploy",
|
|
||||||
"Value": "Yeniden deploy için korundu"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Id": "da051441-4ed5-589a-9a3b-b8066f8a3141",
|
"Id": "da051441-4ed5-589a-9a3b-b8066f8a3141",
|
||||||
"CultureName": "en",
|
"CultureName": "en",
|
||||||
|
|
@ -64269,14 +64255,14 @@
|
||||||
"CultureName": "en",
|
"CultureName": "en",
|
||||||
"ResourceName": "Platform",
|
"ResourceName": "Platform",
|
||||||
"Key": "App.ListForm.WizardImportWizardsApplied",
|
"Key": "App.ListForm.WizardImportWizardsApplied",
|
||||||
"Value": "Screens installed"
|
"Value": "Screens waiting for DB Migrate"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Id": "02d281a8-85a6-57d1-ae67-82e2824e5bea",
|
"Id": "02d281a8-85a6-57d1-ae67-82e2824e5bea",
|
||||||
"CultureName": "tr",
|
"CultureName": "tr",
|
||||||
"ResourceName": "Platform",
|
"ResourceName": "Platform",
|
||||||
"Key": "App.ListForm.WizardImportWizardsApplied",
|
"Key": "App.ListForm.WizardImportWizardsApplied",
|
||||||
"Value": "Kurulan ekranlar"
|
"Value": "DB Migrate bekleyen ekranlar"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Id": "69860bed-53a5-59ee-8252-39f9b54a1554",
|
"Id": "69860bed-53a5-59ee-8252-39f9b54a1554",
|
||||||
|
|
@ -69135,6 +69121,104 @@
|
||||||
"ResourceName": "Platform",
|
"ResourceName": "Platform",
|
||||||
"Key": "App.Listform.ListformField.TwoLetterISOLanguageName",
|
"Key": "App.Listform.ListformField.TwoLetterISOLanguageName",
|
||||||
"Value": "İki Harfli ISO Kodu"
|
"Value": "İki Harfli ISO Kodu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "92e980fc-d1ba-5fa7-becd-b8d906a449ab",
|
||||||
|
"CultureName": "en",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardStatusPending",
|
||||||
|
"Value": "Waiting for migrate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "a08169eb-9b91-5baa-be38-ec0b42b5910e",
|
||||||
|
"CultureName": "tr",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardStatusPending",
|
||||||
|
"Value": "Migrate bekliyor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "02b15695-60a4-5c81-bb0d-384002859e6a",
|
||||||
|
"CultureName": "en",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardStatusInstalled",
|
||||||
|
"Value": "Installed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "5cc4549c-5719-53d4-9db3-35833a147142",
|
||||||
|
"CultureName": "tr",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardStatusInstalled",
|
||||||
|
"Value": "Kurulu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "cb9e2cc1-e6e9-5733-9bcc-89a728cb7fd7",
|
||||||
|
"CultureName": "en",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardStatusPendingHint",
|
||||||
|
"Value": "Seed files are written. The screen, menu and permissions are written to the database by DB Migrate."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "5c1f0e9f-8210-5f28-9a58-54299940c737",
|
||||||
|
"CultureName": "tr",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardStatusPendingHint",
|
||||||
|
"Value": "Seed dosyaları yazıldı. Ekran, menü ve yetkiler DB Migrate ile veritabanına yazılır."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "b1ac9b48-8d1c-5926-9a7c-cfbc15f4f47b",
|
||||||
|
"CultureName": "en",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardStatusInstalledHint",
|
||||||
|
"Value": "Applied to the database by DB Migrate."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "75f80850-d411-5d93-b715-d56fd441f0ba",
|
||||||
|
"CultureName": "tr",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardStatusInstalledHint",
|
||||||
|
"Value": "DB Migrate ile veritabanına uygulandı."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "96cba76a-76bf-5925-9c93-4ce9011f72b3",
|
||||||
|
"CultureName": "en",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardPendingCount",
|
||||||
|
"Value": "{0} waiting for migrate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "797b08b6-7f10-5abf-9381-b9b6ae2b7f4c",
|
||||||
|
"CultureName": "tr",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardPendingCount",
|
||||||
|
"Value": "{0} wizard migrate bekliyor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "5e521ec7-91c6-50fc-892c-e631bee3326a",
|
||||||
|
"CultureName": "en",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardDeployedPendingMigrate",
|
||||||
|
"Value": "Wizard saved to the seed files. Run DB Migrate to create the screen, menu and permissions, then grant the permissions to roles from the role screen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "cc09dc76-7067-5779-bf68-f40c4adc100c",
|
||||||
|
"CultureName": "tr",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.ListForm.WizardDeployedPendingMigrate",
|
||||||
|
"Value": "Wizard seed dosyalarına kaydedildi. Ekranı, menüyü ve yetkileri oluşturmak için DB Migrate çalıştırın; ardından yetkileri rol ekranından rollere verin."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "2df778f2-8575-59a0-b49a-75bcc98a4c13",
|
||||||
|
"CultureName": "en",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.Mcp.Error.VerifyWizardPending",
|
||||||
|
"Value": "Wizard \"{0}\" is not installed yet: its seed files are written but DB Migrate has not run. Ask the user to run DB Migrate from the Wizard Manager, then verify again."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "3217cd45-cac5-5d2c-9964-37627da3b507",
|
||||||
|
"CultureName": "tr",
|
||||||
|
"ResourceName": "Platform",
|
||||||
|
"Key": "App.Mcp.Error.VerifyWizardPending",
|
||||||
|
"Value": "\"{0}\" wizard'ı henüz kurulmadı: seed dosyaları yazıldı ama DB Migrate çalışmadı. Kullanıcıdan Wizard Manager'da DB Migrate çalıştırmasını isteyin, sonra tekrar doğrulayın."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,8 @@ interface DbMigrateButtonProps {
|
||||||
/** Küçük ekranlarda etiketi gizle (yalnızca ikon göster). */
|
/** Küçük ekranlarda etiketi gizle (yalnızca ikon göster). */
|
||||||
hideLabelOnMobile?: boolean
|
hideLabelOnMobile?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
/** Migrate hatasız bittiğinde çağrılır. */
|
||||||
|
onCompleted?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -20,7 +22,11 @@ interface DbMigrateButtonProps {
|
||||||
* toplanmıştır. Yetkisi olmayan kullanıcıya buton hiç render edilmez; asıl kontrol
|
* toplanmıştır. Yetkisi olmayan kullanıcıya buton hiç render edilmez; asıl kontrol
|
||||||
* yine de sunucuda (`App.Setup.Migrate`) yapılır.
|
* yine de sunucuda (`App.Setup.Migrate`) yapılır.
|
||||||
*/
|
*/
|
||||||
const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButtonProps) => {
|
const DbMigrateButton = ({
|
||||||
|
hideLabelOnMobile = false,
|
||||||
|
className,
|
||||||
|
onCompleted,
|
||||||
|
}: DbMigrateButtonProps) => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const { checkPermission } = usePermission()
|
const { checkPermission } = usePermission()
|
||||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false)
|
const [isConfirmOpen, setIsConfirmOpen] = useState(false)
|
||||||
|
|
@ -54,7 +60,7 @@ const DbMigrateButton = ({ hideLabelOnMobile = false, className }: DbMigrateButt
|
||||||
onClose={() => setIsConfirmOpen(false)}
|
onClose={() => setIsConfirmOpen(false)}
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
setIsConfirmOpen(false)
|
setIsConfirmOpen(false)
|
||||||
UiEvalService.ApiDbMigrate()
|
UiEvalService.ApiDbMigrate(onCompleted)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,8 @@ export interface WizardFileInfoDto {
|
||||||
menuIcon: string
|
menuIcon: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
hasInsertedRecords: boolean
|
hasInsertedRecords: boolean
|
||||||
|
/** Dosya veritabanına henüz uygulanmadı; ekran DB Migrate ile kurulur. */
|
||||||
|
isPending: boolean
|
||||||
componentKind: WizardComponentKindEnum
|
componentKind: WizardComponentKindEnum
|
||||||
customComponentName: string
|
customComponentName: string
|
||||||
menuUrl: string
|
menuUrl: string
|
||||||
|
|
@ -176,7 +178,6 @@ export enum WizardDeleteStatusEnum {
|
||||||
export enum WizardDeleteReasonEnum {
|
export enum WizardDeleteReasonEnum {
|
||||||
None = 0,
|
None = 0,
|
||||||
InUse = 1,
|
InUse = 1,
|
||||||
Redeploy = 2,
|
|
||||||
NotSelected = 3,
|
NotSelected = 3,
|
||||||
NotManaged = 4,
|
NotManaged = 4,
|
||||||
Failed = 5,
|
Failed = 5,
|
||||||
|
|
|
||||||
|
|
@ -90,9 +90,10 @@ export abstract class UiEvalService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Canlı sistemde migration tetikler (SQL Query Manager ve Wizard dosya yöneticisi ortak kullanır).
|
* Canlı sistemde migration tetikler (SQL Query Manager ve Wizard dosya yöneticisi ortak kullanır).
|
||||||
* Yetki sunucu tarafında `App.Setup.Migrate` izniyle doğrulanır.
|
* Yetki sunucu tarafında `App.Setup.Migrate` izniyle doğrulanır. `onSuccess`, migrate hatasız
|
||||||
|
* bittiğinde çağrılır; migrate'in kurduğu kayıtları gösteren ekran kendini yeniler.
|
||||||
*/
|
*/
|
||||||
static ApiDbMigrate = () => {
|
static ApiDbMigrate = (onSuccess?: () => void) => {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="info" duration={3000}>
|
<Notification type="info" duration={3000}>
|
||||||
{UiEvalService.translate('::App.Platform.StartMessage')}
|
{UiEvalService.translate('::App.Platform.StartMessage')}
|
||||||
|
|
@ -126,6 +127,7 @@ export abstract class UiEvalService {
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
|
onSuccess?.()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1082,7 +1082,6 @@ const Wizard = () => {
|
||||||
formikRef.current?.setFieldValue('customComponentName', name)
|
formikRef.current?.setFieldValue('customComponentName', name)
|
||||||
formikRef.current?.setFieldValue('menuUrl', routePath)
|
formikRef.current?.setFieldValue('menuUrl', routePath)
|
||||||
}
|
}
|
||||||
const { getConfig } = useStoreActions((a) => a.abpConfig)
|
|
||||||
|
|
||||||
const preventEnterSubmit = (event: KeyboardEvent<HTMLFormElement>) => {
|
const preventEnterSubmit = (event: KeyboardEvent<HTMLFormElement>) => {
|
||||||
if (event.key !== 'Enter') return
|
if (event.key !== 'Enter') return
|
||||||
|
|
@ -1104,10 +1103,23 @@ const Wizard = () => {
|
||||||
const values = formik.values
|
const values = formik.values
|
||||||
const isCustom = values.componentKind === WizardComponentKindEnum.Custom
|
const isCustom = values.componentKind === WizardComponentKindEnum.Custom
|
||||||
|
|
||||||
// Edit modunda eski dosyanın silinmesini (ve DB kayıtlarının temizlenmesini) sunucu yapar;
|
// Edit modunda eski dosyanın yerine yenisini sunucu yazar; istek `editFileName` ile
|
||||||
// istek `editFileName` ile geldiğinde güncelleme yetkisiyle çalışır.
|
// geldiğinde güncelleme yetkisiyle çalışır.
|
||||||
const editing = isEditMode && editFileName ? { editFileName } : {}
|
const editing = isEditMode && editFileName ? { editFileName } : {}
|
||||||
|
|
||||||
|
// Deploy yalnızca seed dosyalarını yazar; ekran, menü ve yetkiler DB Migrate ile kurulur.
|
||||||
|
// Bu yüzden ekranın adresine değil, migrate'in çalıştırıldığı dosya yöneticisine dönülür.
|
||||||
|
const returnToManager = () => {
|
||||||
|
toast.push(
|
||||||
|
<Notification type="success" duration={4000}>
|
||||||
|
{translate('::App.ListForm.WizardDeployedPendingMigrate')}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'bottom-end' },
|
||||||
|
)
|
||||||
|
|
||||||
|
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizardManager, { replace: true })
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Custom yolunda ListForm'a ait hiçbir koleksiyon gönderilmez.
|
// Custom yolunda ListForm'a ait hiçbir koleksiyon gönderilmez.
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
|
|
@ -1119,16 +1131,7 @@ const Wizard = () => {
|
||||||
widgets: [],
|
widgets: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
await getConfig(true)
|
returnToManager()
|
||||||
|
|
||||||
toast.push(
|
|
||||||
<Notification type="success" duration={2000}>
|
|
||||||
{translate('::App.Platform.FormBilgileriKaydedildi')}
|
|
||||||
</Notification>,
|
|
||||||
{ placement: 'bottom-end' },
|
|
||||||
)
|
|
||||||
|
|
||||||
navigate(values.menuUrl, { replace: true })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1168,19 +1171,7 @@ const Wizard = () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Menü/permission değişikliklerinin görünmesi için ABP config'i tazele
|
returnToManager()
|
||||||
await getConfig(true)
|
|
||||||
|
|
||||||
toast.push(
|
|
||||||
<Notification type="success" duration={2000}>
|
|
||||||
{translate('::App.Platform.FormBilgileriKaydedildi')}
|
|
||||||
</Notification>,
|
|
||||||
{ placement: 'bottom-end' },
|
|
||||||
)
|
|
||||||
|
|
||||||
navigate(ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode), {
|
|
||||||
replace: true,
|
|
||||||
})
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger">
|
<Notification type="danger">
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import {
|
||||||
FaLevelUpAlt,
|
FaLevelUpAlt,
|
||||||
FaPlusSquare,
|
FaPlusSquare,
|
||||||
FaMinusSquare,
|
FaMinusSquare,
|
||||||
|
FaHourglassHalf,
|
||||||
|
FaCheckCircle,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import DbMigrateButton from '@/components/shared/DbMigrateButton'
|
import DbMigrateButton from '@/components/shared/DbMigrateButton'
|
||||||
import {
|
import {
|
||||||
|
|
@ -180,6 +182,7 @@ const WizardFileManager = () => {
|
||||||
|
|
||||||
const { states } = useStoreState((state) => state.admin.lists)
|
const { states } = useStoreState((state) => state.admin.lists)
|
||||||
const { setStates } = useStoreActions((a) => a.admin.lists)
|
const { setStates } = useStoreActions((a) => a.admin.lists)
|
||||||
|
const { getConfig } = useStoreActions((a) => a.abpConfig)
|
||||||
const viewMode: ListViewLayoutType =
|
const viewMode: ListViewLayoutType =
|
||||||
states.find((s) => s.listFormCode === VIEW_STATE_CODE)?.layout === 'grid' ? 'grid' : 'card'
|
states.find((s) => s.listFormCode === VIEW_STATE_CODE)?.layout === 'grid' ? 'grid' : 'card'
|
||||||
const setViewMode = (layout: ListViewLayoutType) =>
|
const setViewMode = (layout: ListViewLayoutType) =>
|
||||||
|
|
@ -252,6 +255,15 @@ const WizardFileManager = () => {
|
||||||
loadFiles()
|
loadFiles()
|
||||||
}, [loadFiles])
|
}, [loadFiles])
|
||||||
|
|
||||||
|
// Deploy yalnizca seed dosyalarini yazar; ekran, menu ve yetkiler migrate ile gelir. Migrate
|
||||||
|
// bitince durumlar ve menu (ABP config) yenilenir.
|
||||||
|
const handleMigrateCompleted = useCallback(async () => {
|
||||||
|
await loadFiles()
|
||||||
|
await getConfig(true)
|
||||||
|
}, [loadFiles, getConfig])
|
||||||
|
|
||||||
|
const pendingCount = useMemo(() => files.filter((f) => f.isPending).length, [files])
|
||||||
|
|
||||||
// Zip'i sunucu uretir; istemci yalnizca indirmeyi tetikler. Dosya adi Content-Disposition
|
// Zip'i sunucu uretir; istemci yalnizca indirmeyi tetikler. Dosya adi Content-Disposition
|
||||||
// basligindan okunur, okunamazsa wizard adina duser.
|
// basligindan okunur, okunamazsa wizard adina duser.
|
||||||
/** Arsivi indirir; dosya adi sunucunun verdigi addir, yoksa wizard adina duser. */
|
/** Arsivi indirir; dosya adi sunucunun verdigi addir, yoksa wizard adina duser. */
|
||||||
|
|
@ -476,8 +488,9 @@ const WizardFileManager = () => {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */}
|
{/* Menünün açtığı adres yeni sekmede açılır; adres yoksa ya da ekran henüz kurulmadıysa
|
||||||
{f.menuUrl && (
|
buton anlamsızdır. */}
|
||||||
|
{f.menuUrl && !f.isPending && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
|
@ -542,6 +555,28 @@ const WizardFileManager = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deploy edilmiş ama DB Migrate ile veritabanına uygulanmamış dosya ayrı görünür.
|
||||||
|
const renderStatusBadge = (f: WizardFileInfoDto) => (
|
||||||
|
<span
|
||||||
|
title={translate(
|
||||||
|
f.isPending
|
||||||
|
? '::App.ListForm.WizardStatusPendingHint'
|
||||||
|
: '::App.ListForm.WizardStatusInstalledHint',
|
||||||
|
)}
|
||||||
|
className={classNames(
|
||||||
|
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap',
|
||||||
|
f.isPending
|
||||||
|
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||||
|
: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{f.isPending ? <FaHourglassHalf /> : <FaCheckCircle />}
|
||||||
|
{translate(
|
||||||
|
f.isPending ? '::App.ListForm.WizardStatusPending' : '::App.ListForm.WizardStatusInstalled',
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
const renderMenuPath = (f: WizardFileInfoDto, className?: string) => {
|
const renderMenuPath = (f: WizardFileInfoDto, className?: string) => {
|
||||||
const text = menuPathText(f)
|
const text = menuPathText(f)
|
||||||
if (!text) return null
|
if (!text) return null
|
||||||
|
|
@ -632,7 +667,16 @@ const WizardFileManager = () => {
|
||||||
>
|
>
|
||||||
<FaSync />
|
<FaSync />
|
||||||
</Button>
|
</Button>
|
||||||
<DbMigrateButton hideLabelOnMobile />
|
{pendingCount > 0 && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
|
||||||
|
title={translate('::App.ListForm.WizardStatusPendingHint')}
|
||||||
|
>
|
||||||
|
<FaHourglassHalf />
|
||||||
|
{translate('::App.ListForm.WizardPendingCount', { 0: pendingCount })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<DbMigrateButton hideLabelOnMobile onCompleted={handleMigrateCompleted} />
|
||||||
{canImport && (
|
{canImport && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -734,6 +778,7 @@ const WizardFileManager = () => {
|
||||||
{/* 3. satır: yol ayrımı (Custom / List) ve bileşen kodu */}
|
{/* 3. satır: yol ayrımı (Custom / List) ve bileşen kodu */}
|
||||||
<div className="flex flex-wrap pt-1 items-center gap-1.5 text-xs">
|
<div className="flex flex-wrap pt-1 items-center gap-1.5 text-xs">
|
||||||
{renderKindBadge(f)}
|
{renderKindBadge(f)}
|
||||||
|
{renderStatusBadge(f)}
|
||||||
<div className="text-xs text-gray-400 truncate">{componentName(f)}</div>
|
<div className="text-xs text-gray-400 truncate">{componentName(f)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -868,7 +913,12 @@ const WizardFileManager = () => {
|
||||||
<td className="px-4 py-2 text-gray-600 dark:text-gray-300">
|
<td className="px-4 py-2 text-gray-600 dark:text-gray-300">
|
||||||
{componentName(f)}
|
{componentName(f)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{renderKindBadge(f)}</td>
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{renderKindBadge(f)}
|
||||||
|
{renderStatusBadge(f)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2 text-gray-600 dark:text-gray-300 whitespace-nowrap">
|
<td className="px-4 py-2 text-gray-600 dark:text-gray-300 whitespace-nowrap">
|
||||||
{renderMenuPath(f)}
|
{renderMenuPath(f)}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
rollbackWizardImport,
|
rollbackWizardImport,
|
||||||
} from '@/services/wizard.service'
|
} from '@/services/wizard.service'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { useStoreActions, useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import Input from '@/components/ui/Input'
|
import Input from '@/components/ui/Input'
|
||||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||||
import SqlTableDesignerDialog from '@/views/developerKit/SqlTableDesignerDialog'
|
import SqlTableDesignerDialog from '@/views/developerKit/SqlTableDesignerDialog'
|
||||||
|
|
@ -120,7 +120,6 @@ const ImportDialogBody = ({ children }: { children: ReactNode }) => {
|
||||||
*/
|
*/
|
||||||
const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogProps) => {
|
const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogProps) => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const { getConfig } = useStoreActions((a) => a.abpConfig)
|
|
||||||
const mode = useStoreState((state) => state.theme.mode)
|
const mode = useStoreState((state) => state.theme.mode)
|
||||||
|
|
||||||
const [phase, setPhase] = useState<ImportPhase>('analyzing')
|
const [phase, setPhase] = useState<ImportPhase>('analyzing')
|
||||||
|
|
@ -388,12 +387,8 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
|
||||||
setSchemaApplied(res.data.appliedSchema ?? [])
|
setSchemaApplied(res.data.appliedSchema ?? [])
|
||||||
setCrudApplied(res.data.appliedCrud ?? [])
|
setCrudApplied(res.data.appliedCrud ?? [])
|
||||||
setSeedApplied(res.data.appliedData ?? [])
|
setSeedApplied(res.data.appliedData ?? [])
|
||||||
const installed = res.data.appliedWizards ?? []
|
// Ekranlar import'ta kurulmaz; wizard dosyalari DB Migrate'i bekler.
|
||||||
setWizardsApplied(installed)
|
setWizardsApplied(res.data.appliedWizards ?? [])
|
||||||
|
|
||||||
// Kurulan ekranin menusu ve yetkileri ABP config'inden gelir; yenilenmezse kullanici
|
|
||||||
// ekrani ancak sayfayi bastan yukledikten sonra gorurdu.
|
|
||||||
if (installed.some((row) => !row.error)) await getConfig(true)
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Dosyalar hedefe yazildi; yalnizca veritabanina uygulama ya da temizlik basarisiz oldu.
|
// Dosyalar hedefe yazildi; yalnizca veritabanina uygulama ya da temizlik basarisiz oldu.
|
||||||
setError(err?.response?.data?.error?.message ?? err?.message ?? null)
|
setError(err?.response?.data?.error?.message ?? err?.message ?? null)
|
||||||
|
|
|
||||||
|
|
@ -209,8 +209,8 @@ function Row({ label, value }: { label: string; value?: string | number }) {
|
||||||
// ─── WizardStepDeploy ─────────────────────────────────────────────────────────────
|
// ─── WizardStepDeploy ─────────────────────────────────────────────────────────────
|
||||||
/**
|
/**
|
||||||
* Deploy adımı: önceki adımlarda toplanan her şeyin özetini gösterir ve onay sonrası
|
* Deploy adımı: önceki adımlarda toplanan her şeyin özetini gösterir ve onay sonrası
|
||||||
* wizard'ı sunucuya gönderir. Sunucu bu istekten menü, izin, dil, ListForm ve seed
|
* wizard'ı sunucuya gönderir. Sunucu menü, izin, dil ve ListForm'u yalnızca seed dosyalarına
|
||||||
* dosyasını üretir; özet ekranında bağlantı cümlesi maskelenerek gösterilir.
|
* yazar; veritabanına DB Migrate ile geçer. Özet ekranında bağlantı cümlesi maskelenerek gösterilir.
|
||||||
*/
|
*/
|
||||||
const WizardStepDeploy = ({
|
const WizardStepDeploy = ({
|
||||||
values,
|
values,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue