LangugesText dosya düzenlemesi
This commit is contained in:
parent
23667314ee
commit
d99ac59cdc
39 changed files with 738686 additions and 49336 deletions
19
.github/instructions/list.instructions.md
vendored
19
.github/instructions/list.instructions.md
vendored
|
|
@ -18,7 +18,7 @@ This document summarizes the rules, standards, and step-by-step instructions for
|
||||||
- Table names: `{Modul}_T_{Entity}` (e.g., `Mrp_T_Order`)
|
- Table names: `{Modul}_T_{Entity}` (e.g., `Mrp_T_Order`)
|
||||||
- Menu/permission keys: `{Modul}`, `{Modul}.{Liste}`
|
- Menu/permission keys: `{Modul}`, `{Modul}.{Liste}`
|
||||||
- **Tenant Isolation:** All data, forms, and permissions must be tenant-aware.
|
- **Tenant Isolation:** All data, forms, and permissions must be tenant-aware.
|
||||||
- **Localization:** Every menu, list, and field must have a corresponding entry in `configs/seeds/data/Sas.Languages.LanguageText.json`.
|
- **Localization:** Every menu, list, and field must have a corresponding entry in `configs/seeds/data/Sas.Languages.LanguageText/{CultureName}.json`.
|
||||||
- **No Redundant Code:** Avoid duplicating permission grants or seeding logic across modules.
|
- **No Redundant Code:** Avoid duplicating permission grants or seeding logic across modules.
|
||||||
- **SeedConsts Rule:** For every new module/entity, add a constant to `SeedConsts` (e.g., `public static class {Modul}` and `public const string {Liste}`) if it does not already exist. Never duplicate existing constants.
|
- **SeedConsts Rule:** For every new module/entity, add a constant to `SeedConsts` (e.g., `public static class {Modul}` and `public const string {Liste}`) if it does not already exist. Never duplicate existing constants.
|
||||||
- **DeleteCommand Rule:** In all `ListFormSeeder_{Modul}.cs` files, the `DeleteCommand` property **must** use the `DefaultDeleteCommand` function, e.g., `DeleteCommand = DefaultDeleteCommand("{TabloAdı}")`. Never use a raw SQL string directly for `DeleteCommand`.
|
- **DeleteCommand Rule:** In all `ListFormSeeder_{Modul}.cs` files, the `DeleteCommand` property **must** use the `DefaultDeleteCommand` function, e.g., `DeleteCommand = DefaultDeleteCommand("{TabloAdı}")`. Never use a raw SQL string directly for `DeleteCommand`.
|
||||||
|
|
@ -30,9 +30,12 @@ This document summarizes the rules, standards, and step-by-step instructions for
|
||||||
- **configs/seeds/data/Sas.Menus.*.json:**
|
- **configs/seeds/data/Sas.Menus.*.json:**
|
||||||
- Never add, remove or re-path a route in `Sas.Menus.Routes.json`. The only field set you may edit on an existing row is its `Seo*` columns (lowcode §0.9). Only `Sas.Menus.Module.json` and `Sas.Menus.Menu.json` can be changed for menu additions or updates. All menu additions must use the platform's configuration mechanisms or be added to those two files as required by platform design.
|
- Never add, remove or re-path a route in `Sas.Menus.Routes.json`. The only field set you may edit on an existing row is its `Seo*` columns (lowcode §0.9). Only `Sas.Menus.Module.json` and `Sas.Menus.Menu.json` can be changed for menu additions or updates. All menu additions must use the platform's configuration mechanisms or be added to those two files as required by platform design.
|
||||||
|
|
||||||
- **configs/seeds/data/Sas.Languages.LanguageText.json:**
|
- **configs/seeds/data/Sas.Languages.LanguageText/{CultureName}.json:**
|
||||||
- When adding a new key, always check if the key already exists. Only add the key if it does not exist to prevent duplicates.
|
- When adding a new key, always check if the key already exists. Only add the key if it does not exist to prevent duplicates.
|
||||||
- Each key is two rows (`CultureName` `en` and `tr`). `Id` = UUID v5 (URL namespace) of `Sas_H_LanguageText:{ResourceName}:{Key}:{CultureName}`; never a random GUID.
|
- The data lives in the folder `configs/seeds/data/Sas.Languages.LanguageText/`: one file per language (`ar, de-DE, en, es, fi, fr, hi, hr, hu, it, pt-BR, ru, sk, sl, tr, zh-Hans`, i.e. every `CultureName` of `Sas.Languages.Language.json`). The file name is the language's `CultureName` (`{CultureName}.json`: `tr.json`, `de-DE.json`) and its `Rows` hold all rows of that language.
|
||||||
|
- Wizard, MCP and every component/screen/menu addition follow the same rule: the generated EN + TR rows are written by the deploy/seeder, the producer then writes the translated rows for all other languages (`lowcode.instructions.md` §4.4.3, CLAUDE.md standing default 27). Deleting a key removes its row from every file.
|
||||||
|
- Every new key is added to **all** language files (one row per file). `en` and `tr` carry the authored text; the other 14 files get its translation. When a language is added to `Sas.Languages.Language.json`, create its file and translate every key.
|
||||||
|
- Each row's `Id` = UUID v5 (URL namespace) of `Sas_H_LanguageText:{ResourceName}:{Key}:{CultureName}`; never a random GUID.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -61,7 +64,7 @@ This document summarizes the rules, standards, and step-by-step instructions for
|
||||||
### 6. PlatformIdentityDataSeeder.cs
|
### 6. PlatformIdentityDataSeeder.cs
|
||||||
- **Removed** redundant {Modul} permission grant logic (now handled by the permission seed data).
|
- **Removed** redundant {Modul} permission grant logic (now handled by the permission seed data).
|
||||||
|
|
||||||
### 7. Sas.Languages.LanguageText.json
|
### 7. Sas.Languages.LanguageText/{CultureName}.json
|
||||||
- **Added** localization entries for:
|
- **Added** localization entries for:
|
||||||
- {Modul} menu and {Liste} list
|
- {Modul} menu and {Liste} list
|
||||||
- {Liste} list fields (e.g., `FullName`, `Phone`)
|
- {Liste} list fields (e.g., `FullName`, `Phone`)
|
||||||
|
|
@ -78,7 +81,7 @@ Yeni bir modül ekle (ör: {Modul}):
|
||||||
- configs/seeds/data/Abp.Identity.PermissionGroups.json'da ayrı bir grup oluştur
|
- configs/seeds/data/Abp.Identity.PermissionGroups.json'da ayrı bir grup oluştur
|
||||||
- ListFormSeeder_{Modul}.cs dosyası oluştur ve list-form seed'ini buraya taşı
|
- ListFormSeeder_{Modul}.cs dosyası oluştur ve list-form seed'ini buraya taşı
|
||||||
- SqlObjects.sql'de tabloyu {Modul}_T_{Entity} olarak adlandır
|
- SqlObjects.sql'de tabloyu {Modul}_T_{Entity} olarak adlandır
|
||||||
- configs/seeds/data/Sas.Languages.LanguageText.json dosyasına menü, liste ve alan çevirilerini ekle
|
- configs/seeds/data/Sas.Languages.LanguageText/{CultureName}.json dosyasına menü, liste ve alan çevirilerini ekle
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Add a New List Under a Module
|
### 2. Add a New List Under a Module
|
||||||
|
|
@ -88,7 +91,7 @@ Yeni bir liste ekle (ör: {Liste}):
|
||||||
- ListFormSeeder_{Modul}.cs dosyasına seed kodunu ekle
|
- ListFormSeeder_{Modul}.cs dosyasına seed kodunu ekle
|
||||||
- SqlObjects.sql'de tabloyu {Modul}_T_{Entity} olarak oluştur
|
- SqlObjects.sql'de tabloyu {Modul}_T_{Entity} olarak oluştur
|
||||||
- configs/seeds/data/Abp.Identity.Permissions.json'da ilgili izinleri {Modul} grubuna ekle
|
- configs/seeds/data/Abp.Identity.Permissions.json'da ilgili izinleri {Modul} grubuna ekle
|
||||||
- configs/seeds/data/Sas.Languages.LanguageText.json dosyasına liste ve alan çevirilerini ekle
|
- configs/seeds/data/Sas.Languages.LanguageText/{CultureName}.json dosyasına liste ve alan çevirilerini ekle
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Enforce Tenant Isolation and Full Audit
|
### 3. Enforce Tenant Isolation and Full Audit
|
||||||
|
|
@ -101,7 +104,7 @@ Tablo ve list-form için tenant izolasyonu ve full-audit alanlarını ekle:
|
||||||
|
|
||||||
### 4. Add/Update Localization
|
### 4. Add/Update Localization
|
||||||
```
|
```
|
||||||
Yeni menü, liste veya alan eklediğinde configs/seeds/data/Sas.Languages.LanguageText.json dosyasına (EN + TR satırı) şu şekilde ekle:
|
Yeni menü, liste veya alan eklediğinde configs/seeds/data/Sas.Languages.LanguageText/{CultureName}.json klasöründeki tüm dil dosyalarına (EN + TR yazılan metin, diğer diller çeviri) şu şekilde ekle:
|
||||||
- {Modul}
|
- {Modul}
|
||||||
- {Modul}.{Liste}
|
- {Modul}.{Liste}
|
||||||
- Sas.Listform.ListformField.{Alan}
|
- Sas.Listform.ListformField.{Alan}
|
||||||
|
|
@ -128,7 +131,7 @@ Yeni menü, liste veya alan eklediğinde configs/seeds/data/Sas.Languages.Langua
|
||||||
- [ ] Menü ve izinler doğru hiyerarşide mi?
|
- [ ] Menü ve izinler doğru hiyerarşide mi?
|
||||||
- [ ] List-form seed'leri doğru modül dosyasında mı?
|
- [ ] List-form seed'leri doğru modül dosyasında mı?
|
||||||
- [ ] Tablo isimleri ve referansları standartlara uygun mu? (örn: {Modul}_T_{Entity})
|
- [ ] Tablo isimleri ve referansları standartlara uygun mu? (örn: {Modul}_T_{Entity})
|
||||||
- [ ] Sas.Languages.LanguageText.json'da tüm yeni menü, liste ve alanlar var mı?
|
- [ ] Sas.Languages.LanguageText/{CultureName}.json'da tüm yeni menü, liste ve alanlar var mı?
|
||||||
- [ ] Redundant kod veya izin grant'ı var mı?
|
- [ ] Redundant kod veya izin grant'ı var mı?
|
||||||
- [ ] Seed ve migration sonrası UI'da çeviriler doğru görünüyor mu?
|
- [ ] Seed ve migration sonrası UI'da çeviriler doğru görünüyor mu?
|
||||||
- [ ] SeedConsts.cs dosyasında ilgili sabitler var mı, tekrar eklenmemiş mi?
|
- [ ] SeedConsts.cs dosyasında ilgili sabitler var mı, tekrar eklenmemiş mi?
|
||||||
|
|
|
||||||
|
|
@ -1484,7 +1484,7 @@ soneklerini alır.
|
||||||
| **Tenants** | `AbpTenantManagement.Tenants` | `AbpTenants` | host | Kiracı kaydı, bağlantı dizesi diyaloğu (`TenantsConnectionString`), alt form olarak şubeler; yazma `list-form-dynamic-api/tenant-*`. Ekranların `IsTenant` davranışının kaynağı. |
|
| **Tenants** | `AbpTenantManagement.Tenants` | `AbpTenants` | host | Kiracı kaydı, bağlantı dizesi diyaloğu (`TenantsConnectionString`), alt form olarak şubeler; yazma `list-form-dynamic-api/tenant-*`. Ekranların `IsTenant` davranışının kaynağı. |
|
||||||
| **Branches** | `Sas.Branches` | `Sas_T_Branch` | tenant | Şube listesi. Ekranı `IsBranch = true` yaparsan filtre bu tablodan gelir; `BranchSeed` diyaloğu şube verisini kurar; `Sas.Branches.Update` ister ve kiracı yalnızca kendi şubesini kurabilir. |
|
| **Branches** | `Sas.Branches` | `Sas_T_Branch` | tenant | Şube listesi. Ekranı `IsBranch = true` yaparsan filtre bu tablodan gelir; `BranchSeed` diyaloğu şube verisini kurar; `Sas.Branches.Update` ister ve kiracı yalnızca kendi şubesini kurabilir. |
|
||||||
| **Setting Definitions** | `Sas.SettingDefinitions` | `Sas_H_SettingDefinition` | host | Yeni uygulama ayarı: `Code`, `MainGroupKey`/`SubGroupKey`, `DataType`, `SelectOptions` (`List` tipinde değer → etiket; etiket dil anahtarıdır, Ayarlar ekranı `::` önekiyle çevirir, anahtar yoksa metin olduğu gibi görünür), `DefaultValue`, `Providers`, `RequiredPermissionName`, `IsEncrypted`, `IsVisibleToClients`. Hazır tanımlar `data/Sas.SettingDefinitions.json`'dan gelir. Ayarlanabilir olması gereken değeri koda gömme. |
|
| **Setting Definitions** | `Sas.SettingDefinitions` | `Sas_H_SettingDefinition` | host | Yeni uygulama ayarı: `Code`, `MainGroupKey`/`SubGroupKey`, `DataType`, `SelectOptions` (`List` tipinde değer → etiket; etiket dil anahtarıdır, Ayarlar ekranı `::` önekiyle çevirir, anahtar yoksa metin olduğu gibi görünür), `DefaultValue`, `Providers`, `RequiredPermissionName`, `IsEncrypted`, `IsVisibleToClients`. Hazır tanımlar `data/Sas.SettingDefinitions.json`'dan gelir. Ayarlanabilir olması gereken değeri koda gömme. |
|
||||||
| **Languages › Language / Language Text** | `Sas.Languages.Language` · `.LanguageText` | `Sas_H_Language` · `Sas_H_LanguageKey` · `Sas_H_LanguageText` | host | Dil anahtarları. Üretilen her ekranın EN+TR metinleri buraya yazılır; ekran `data/Sas.Languages.{Language,LanguageText}.json` dosyalarına senkronlanır. Metin insert'i eksik `LanguageKey`'i de üretir, aynı kültür+kaynak+anahtar varsa eklemez. |
|
| **Languages › Language / Language Text** | `Sas.Languages.Language` · `.LanguageText` | `Sas_H_Language` · `Sas_H_LanguageKey` · `Sas_H_LanguageText` | host | Dil anahtarları. Üretilen her ekranın EN+TR metinleri buraya yazılır; ekran `data/Sas.Languages.Language.json + Sas.Languages.LanguageText/{CultureName}.json` dosyalarına senkronlanır. Metin insert'i eksik `LanguageKey`'i de üretir, aynı kültür+kaynak+anahtar varsa eklemez. |
|
||||||
| **Data Source** | `Sas.DataSource` | `Sas_H_DataSource` | host | `Code`, `DataSourceType`, `ConnectionString`. Tipli API (`/api/app/data-source`) bağlantı cümlesini hiçbir zaman döndürmez; okuma açıktır (açılır listeler), yazma `Sas.DataSource.Create/Update/Delete` ister. **İki tür kayıt bir arada:** elle tanımlanan host/harici bağlantılar ve `Code = tenant adı` olan otomatik tenant bağlantıları (Tenants ekranındaki bağlantı diyaloğu yönetir, elle dokunma). Varsayılan kaydı `HostDataSeeder` appsettings'teki bağlantı cümlesinden kurar. Çözüm kuralı `lowcode` §9.3.3. Bağlantı dizesi koda/JSON'a yazılmaz. |
|
| **Data Source** | `Sas.DataSource` | `Sas_H_DataSource` | host | `Code`, `DataSourceType`, `ConnectionString`. Tipli API (`/api/app/data-source`) bağlantı cümlesini hiçbir zaman döndürmez; okuma açıktır (açılır listeler), yazma `Sas.DataSource.Create/Update/Delete` ister. **İki tür kayıt bir arada:** elle tanımlanan host/harici bağlantılar ve `Code = tenant adı` olan otomatik tenant bağlantıları (Tenants ekranındaki bağlantı diyaloğu yönetir, elle dokunma). Varsayılan kaydı `HostDataSeeder` appsettings'teki bağlantı cümlesinden kurar. Çözüm kuralı `lowcode` §9.3.3. Bağlantı dizesi koda/JSON'a yazılmaz. |
|
||||||
| **Background Workers › Recurring Jobs / Jobs** | `Sas.BackgroundWorkers.RecurringJobs` · `.Jobs` | `Sas_H_BackgroundWorker` · `Sas_H_BackgroundWorker_JobFlow` | host | Cron'lu işler ve çalıştırma geçmişi (`Tenant`, `ExecutionDate`, `Duration`, `Status`, `FailureReason`, `ExceptionDetails`). `WorkerType`/`Scope` değerleri §11'de; her kapsam ayrı bir Hangfire işidir, `Tenant` kolonu hangi kiracıda çalıştığını gösterir. Hazır işler (oturum temizliği, bildirim, yedek, şema sözlüğü) `data/Sas.BackgroundWorkers.RecurringJobs.json`'dan gelir. Ayrıntı: `lowcode` §9.10, `README.md` §14. |
|
| **Background Workers › Recurring Jobs / Jobs** | `Sas.BackgroundWorkers.RecurringJobs` · `.Jobs` | `Sas_H_BackgroundWorker` · `Sas_H_BackgroundWorker_JobFlow` | host | Cron'lu işler ve çalıştırma geçmişi (`Tenant`, `ExecutionDate`, `Duration`, `Status`, `FailureReason`, `ExceptionDetails`). `WorkerType`/`Scope` değerleri §11'de; her kapsam ayrı bir Hangfire işidir, `Tenant` kolonu hangi kiracıda çalıştığını gösterir. Hazır işler (oturum temizliği, bildirim, yedek, şema sözlüğü) `data/Sas.BackgroundWorkers.RecurringJobs.json`'dan gelir. Ayrıntı: `lowcode` §9.10, `README.md` §14. |
|
||||||
| **Menus › Routes / Menu / Manager** · **Definitions › Module** | `Sas.Menus.Module` · `.Routes` · `.Menu` · `.Manager` (`/admin/menuManager`) | `Sas_H_Module` · `Sas_H_Route` · `Sas_H_Menu` | host | Menü ve route veritabanından gelir; üretilen ekranın menü kaydı buradan açılır. `Menu` ekranı ağaç düzeninin referansıdır. `Module` ekranının menü ve yetkisi Saas › Definitions altındadır (Applications ekranının hemen altı); ekran kodu `Sas.Menus.Module` olarak kalır. Seed: `data/Sas.Menus.{Routes,Module,Menu}.json` — `Routes` dosyasına yalnızca var olan satırın `Seo*` alanları için dokunulur. |
|
| **Menus › Routes / Menu / Manager** · **Definitions › Module** | `Sas.Menus.Module` · `.Routes` · `.Menu` · `.Manager` (`/admin/menuManager`) | `Sas_H_Module` · `Sas_H_Route` · `Sas_H_Menu` | host | Menü ve route veritabanından gelir; üretilen ekranın menü kaydı buradan açılır. `Menu` ekranı ağaç düzeninin referansıdır. `Module` ekranının menü ve yetkisi Saas › Definitions altındadır (Applications ekranının hemen altı); ekran kodu `Sas.Menus.Module` olarak kalır. Seed: `data/Sas.Menus.{Routes,Module,Menu}.json` — `Routes` dosyasına yalnızca var olan satırın `Seo*` alanları için dokunulur. |
|
||||||
|
|
@ -1718,7 +1718,7 @@ alanları da doldurur (`lowcode` §0.9).
|
||||||
| "barkod/QR basılsın" | Visual Designer › `BarcodeViewer` | `value`'yu `Form` kolonuna bağla |
|
| "barkod/QR basılsın" | Visual Designer › `BarcodeViewer` | `value`'yu `Form` kolonuna bağla |
|
||||||
| "başka veritabanından gelsin" | Saas › Data Source | Yeni DataSource + ekranın `DataSourceCode` alanı |
|
| "başka veritabanından gelsin" | Saas › Data Source | Yeni DataSource + ekranın `DataSourceCode` alanı |
|
||||||
| "ayarlanabilir olsun" | Saas › Setting Definitions | Tanım aç, değeri `/admin/settings`'ten yönet |
|
| "ayarlanabilir olsun" | Saas › Setting Definitions | Tanım aç, değeri `/admin/settings`'ten yönet |
|
||||||
| "İngilizcesi de olsun" | Saas › Languages | Dil anahtarı + `data/Sas.Languages.LanguageText.json` |
|
| "İngilizcesi de olsun" | Saas › Languages | Dil anahtarı + `data/Sas.Languages.LanguageText/{CultureName}.json` |
|
||||||
| "aramada çıksın" | Saas › Definitions › Global Search | `System`/`Group`/`Term`/`Weight`/`Url` kaydı |
|
| "aramada çıksın" | Saas › Definitions › Global Search | `System`/`Group`/`Term`/`Weight`/`Url` kaydı |
|
||||||
| "sadece şu rol görsün" | Administration › Identity › Roles / Permissions | Yetki kodu + ekranın `PermissionJson`'ı |
|
| "sadece şu rol görsün" | Administration › Identity › Roles / Permissions | Yetki kodu + ekranın `PermissionJson`'ı |
|
||||||
| "herkes yalnızca kendi kayıtlarını görsün" / "şu rol şu bölgeyi görsün" | ListForm editörü › Kişiselleştirme | `ServerWhere` kaydı (herkes/rol/kullanıcı; §2.5) ya da `DefaultFilter` |
|
| "herkes yalnızca kendi kayıtlarını görsün" / "şu rol şu bölgeyi görsün" | ListForm editörü › Kişiselleştirme | `ServerWhere` kaydı (herkes/rol/kullanıcı; §2.5) ya da `DefaultFilter` |
|
||||||
|
|
|
||||||
63
.github/instructions/lowcode.instructions.md
vendored
63
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -71,7 +71,7 @@ kurulur; host veritabanını paylaşan tenant'ta dosyalar yeniden uygulanmaz (`S
|
||||||
| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı. MCP'de yeni tablonun scripti `create_table` ile yazılır (biçim tasarımcıyla aynı; tablo DB Migrate'te kurulur); view/procedure/function scripti `save_sql_object` ile iki sağlayıcı için yazılır |
|
| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı. MCP'de yeni tablonun scripti `create_table` ile yazılır (biçim tasarımcıyla aynı; tablo DB Migrate'te kurulur); view/procedure/function scripti `save_sql_object` ile iki sağlayıcı için yazılır |
|
||||||
| 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/Sas.DeveloperKit.CustomComponents/{Id}.json` | Wizard `Custom` yolunda bileşen adını arar |
|
| 3 | Custom Component (gerekiyorsa) | `data/Sas.DeveloperKit.CustomComponents/{Id}.json` | Wizard `Custom` yolunda bileşen adını arar |
|
||||||
| 4 | Wizard | `wizard/{Ad}.json` + menü/yetki/dil satırları (`data/Sas.Menus.*`, `data/Abp.Identity.Permission*`, `data/Sas.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. Dosya ekranı yalnızca `Wizard` bölümüyle taşır; `Data` başlığı (`SelectCommand`/`KeyFieldName`) olmayan List dosyasını seeder atlar (§4.1) |
|
| 4 | Wizard | `wizard/{Ad}.json` + menü/yetki/dil satırları (`data/Sas.Menus.*`, `data/Abp.Identity.Permission*`, `data/Sas.Languages.LanguageText/{CultureName}.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. Dosya ekranı yalnızca `Wizard` bölümüyle taşır; `Data` başlığı (`SelectCommand`/`KeyFieldName`) olmayan List dosyasını 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/Sas.Menus.Routes.json` ya da `data/Sas.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/Sas.Menus.Routes.json` ya da `data/Sas.DeveloperKit.CustomComponents/{Id}.json` | Yolu üreten dosya yazıldıktan sonra; ziyaretçiye açık hiçbir yol metasız kalmaz (§0.9) |
|
||||||
|
|
@ -94,7 +94,7 @@ Uydurma. Şu üçünü mutlaka kontrol et:
|
||||||
`data/Sas.Menus.Menu.json` (platform menüleri), `data/Abp.Identity.PermissionGroups.json` ve
|
`data/Sas.Menus.Menu.json` (platform menüleri), `data/Abp.Identity.PermissionGroups.json` ve
|
||||||
mevcut wizard dosyalarındaki `MenuParentCode` / `PermissionGroupName` değerlerini tara
|
mevcut wizard dosyalarındaki `MenuParentCode` / `PermissionGroupName` değerlerini tara
|
||||||
(MCP'de `list_wizards`).
|
(MCP'de `list_wizards`).
|
||||||
3. **Dil anahtarı çakışması** — `data/Sas.Languages.LanguageText.json` ve diğer wizard dosyaları. Var olan bir
|
3. **Dil anahtarı çakışması** — `data/Sas.Languages.LanguageText/{CultureName}.json` ve diğer wizard dosyaları. Var olan bir
|
||||||
anahtarın metnini seed **ezmez**; yanlış metinle karşılaşırsan anahtarı değiştir.
|
anahtarın metnini seed **ezmez**; yanlış metinle karşılaşırsan anahtarı değiştir.
|
||||||
|
|
||||||
### 0.4 Üretim sonrası
|
### 0.4 Üretim sonrası
|
||||||
|
|
@ -505,7 +505,7 @@ ya da ziyaretçiye açık yeni bir fiziksel sayfa ürettiğinde.
|
||||||
|
|
||||||
| Kural | Neden |
|
| Kural | Neden |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Metin alanları **dil anahtarı** taşır (`::Sas.PublicSeo.{Sayfa}.{Alan}`), anahtarlar `data/Sas.Languages.LanguageText.json`'a EN + TR yazılır | Kayıt kültür başına çoğalmaz; çeviri dil dosyasında, tek yerde durur (standing default 4) |
|
| Metin alanları **dil anahtarı** taşır (`::Sas.PublicSeo.{Sayfa}.{Alan}`), anahtarlar `data/Sas.Languages.LanguageText/{CultureName}.json`'a EN + TR yazılır | Kayıt kültür başına çoğalmaz; çeviri dil dosyasında, tek yerde durur (standing default 4) |
|
||||||
| `Title` 60, `Description` 160 karakteri aşmaz | Arama sonucunda kırpılır |
|
| `Title` 60, `Description` 160 karakteri aşmaz | Arama sonucunda kırpılır |
|
||||||
| Metni uydurma — sayfanın kendi içeriğinden türet | Sayfada yazmayan bir vaat meta'da da yazmamalı |
|
| Metni uydurma — sayfanın kendi içeriğinden türet | Sayfada yazmayan bir vaat meta'da da yazmamalı |
|
||||||
| Kişiye özel / tek kullanımlık akış (`/checkout`, `/payment`, `/success`) → `SeoNoIndex: true`, `SeoIncludeInSitemap: false` | İçeriği olmayan, tekrar ziyaret edilemeyen adres |
|
| Kişiye özel / tek kullanımlık akış (`/checkout`, `/payment`, `/success`) → `SeoNoIndex: true`, `SeoIncludeInSitemap: false` | İçeriği olmayan, tekrar ziyaret edilemeyen adres |
|
||||||
|
|
@ -1165,7 +1165,7 @@ hiçbir şey silmez**. Tek çıktısı seed dosyalarıdır:
|
||||||
| `wizard/{WizardName}.json` | `Wizard` bölümü (wizard cevapları = ekranın bütün sözleşmesi, §4.1), `InsertedRecords`, `Pending`, `DevRequest` |
|
| `wizard/{WizardName}.json` | `Wizard` bölümü (wizard cevapları = ekranın bütün sözleşmesi, §4.1), `InsertedRecords`, `Pending`, `DevRequest` |
|
||||||
| `data/Sas.Menus.Module.json`, `data/Sas.Menus.Menu.json` | Modül, üst menü, ekranın menüsü |
|
| `data/Sas.Menus.Module.json`, `data/Sas.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/Abp.Identity.PermissionGroups.json`, `data/Abp.Identity.Permissions.json` | İzin grubu, okuma yetkisi ve alt yetkileri |
|
||||||
| `data/Sas.Languages.LanguageText.json` | Menü, başlık, açıklama, grup, alan başlıkları ve `Language.Texts` anahtarlarının (widget başlıkları) EN + TR metinleri (`Id` iş anahtarından v5 GUID, `SeedRowIds.LanguageText`) |
|
| `data/Sas.Languages.LanguageText/{CultureName}.json` | Menü, başlık, açıklama, grup, alan başlıkları ve `Language.Texts` anahtarlarının (widget başlıkları) EN + TR metinleri; klasördeki diğer dil dosyalarına çevirileri eklenir (`Id` iş anahtarından v5 GUID, `SeedRowIds.LanguageText`) |
|
||||||
|
|
||||||
Satırları `WizardSeedRowStore` yazar; anahtarlar `WizardSeedRowKeys`'ten gelir (export aynı
|
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
|
listeyle toplar). **Sahiplik:** bir satır ne veritabanında ne dosyada varsa ya da önceki sürüm ona
|
||||||
|
|
@ -1748,7 +1748,7 @@ Bağımlı liste `grouped` ile değil cascade ile kurulur.
|
||||||
|
|
||||||
RadioGroup, SelectBox, TagBox, Autocomplete gibi `StaticData` lookup'ının seçenek metni (`name`) ekran
|
RadioGroup, SelectBox, TagBox, Autocomplete gibi `StaticData` lookup'ının seçenek metni (`name`) ekran
|
||||||
diline göre çevrilir; bu yüzden düz metin değil `Sas.StaticLookup.*` dil anahtarıdır ve anahtarın EN + TR
|
diline göre çevrilir; bu yüzden düz metin değil `Sas.StaticLookup.*` dil anahtarıdır ve anahtarın EN + TR
|
||||||
satırları `data/Sas.Languages.LanguageText.json`'a yazılır (örnek: `ListFormSeeder_Adm`
|
satırları `data/Sas.Languages.LanguageText/{CultureName}.json`'a yazılır (örnek: `ListFormSeeder_Adm`
|
||||||
iş tipleri, `Sas.StaticLookup.MailQueueWorker`).
|
iş tipleri, `Sas.StaticLookup.MailQueueWorker`).
|
||||||
|
|
||||||
Wizard ve MCP deploy'u bunu kendisi yapar (`ListFormWizardAppService.LocalizeStaticLookupsAsync`):
|
Wizard ve MCP deploy'u bunu kendisi yapar (`ListFormWizardAppService.LocalizeStaticLookupsAsync`):
|
||||||
|
|
@ -2205,7 +2205,7 @@ ile ekler (§4.0). Sıra değerleri veritabanı ile dosyadaki satırların birle
|
||||||
editöründen yazmalısın.
|
editöründen yazmalısın.
|
||||||
- Dil anahtarı zaten varsa metni **ezilmez**. Yanlış metin görüyorsan anahtar başka bir yerde
|
- Dil anahtarı zaten varsa metni **ezilmez**. Yanlış metin görüyorsan anahtar başka bir yerde
|
||||||
tanımlanmıştır. Anahtar yalnızca veritabanında varsa (seed dosyasında satırı yoksa) deploy onu
|
tanımlanmıştır. Anahtar yalnızca veritabanında varsa (seed dosyasında satırı yoksa) deploy onu
|
||||||
veritabanındaki metniyle `Sas.Languages.LanguageText.json`'a taşır; sahipliğini almaz.
|
veritabanındaki metniyle `Sas.Languages.LanguageText/{CultureName}.json`'a taşır; sahipliğini almaz.
|
||||||
|
|
||||||
### 4.4.2 Ekran açıklaması — başlıktaki ⓘ (`DescTr` / `DescEn`)
|
### 4.4.2 Ekran açıklaması — başlıktaki ⓘ (`DescTr` / `DescEn`)
|
||||||
|
|
||||||
|
|
@ -2224,9 +2224,27 @@ Biçim `RichText` sözleşmesidir (HTML yazılmaz): her satır bir blok, `• `
|
||||||
madde listesi, `Etiket: …` satırı kalın başlık, maddede `Ad → açıklama` kalıbı. Wizard ekranında
|
madde listesi, `Etiket: …` satırı kalın başlık, maddede `Ad → açıklama` kalıbı. Wizard ekranında
|
||||||
alan çok satırlıdır. MCP `deploy_wizard` her iki dilde **en az 3 satır ve 150 karakter** olmayan
|
alan çok satırlıdır. MCP `deploy_wizard` her iki dilde **en az 3 satır ve 150 karakter** olmayan
|
||||||
açıklamayı reddeder (`McpErrorCodes.WizardDescriptionTooShort`). Metin değiştirildiğinde dil
|
açıklamayı reddeder (`McpErrorCodes.WizardDescriptionTooShort`). Metin değiştirildiğinde dil
|
||||||
satırı `data/Sas.Languages.LanguageText.json`'a yazılır; kurulu ekranda yeni metin wizard
|
satırı `data/Sas.Languages.LanguageText/{CultureName}.json`'a yazılır; kurulu ekranda yeni metin wizard
|
||||||
yeniden deploy edilip (bekleyen kurulum) DB Migrate çalışınca gelir.
|
yeniden deploy edilip (bekleyen kurulum) DB Migrate çalışınca gelir.
|
||||||
|
|
||||||
|
### 4.4.3 Çok dilli metin — her anahtar bütün dil dosyalarına
|
||||||
|
|
||||||
|
Wizard, MCP, Custom Component ya da elle üretilen her ekranın dil anahtarı `data/Sas.Languages.LanguageText/`
|
||||||
|
klasöründeki **bütün** dil dosyalarında bulunur (dosya listesi = `Sas.Languages.Language.json`'daki her `CultureName`).
|
||||||
|
|
||||||
|
| Yol | EN + TR satırı | Diğer dillerin satırı |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Wizard ekranı / `deploy_wizard` | Deploy yazar (`Language.*En/Tr`, `Language.Texts`, sabit lookup anahtarları) | Üreten, deploy'dan sonra her anahtar için kalan dillerin satırlarını ilgili dil dosyasına yazar (MCP: `Sas.Languages.LanguageText` ekranına `insert_row`, dil başına bir satır; Claude Code: dosyaya doğrudan) |
|
||||||
|
| MCP `save_component`, `save_report_category/template` ve satır araçları | Anahtar metni EN + TR yazılır | Aynı şekilde diğer diller |
|
||||||
|
| Custom Component / Visual Designer | Bileşenin `::Anahtar` metinleri (SEO dahil) EN + TR | Aynı şekilde diğer diller |
|
||||||
|
| Menü / yetki grubu / widget başlığı / `Sas.StaticLookup.*` | Deploy ya da seeder yazar | Aynı şekilde diğer diller |
|
||||||
|
|
||||||
|
Kurallar: (1) satır `ResourceName` + `Key` + `CultureName` üçlüsüyle tektir; `Id` = `Sas_H_LanguageText:{ResourceName}:{Key}:{CultureName}`
|
||||||
|
v5 GUID. (2) Çeviri yer tutucuyu (`{0}`), HTML etiketini, kodu ve marka adını aynen korur. (3) Dil listesi koda gömülmez;
|
||||||
|
`Sas.Languages.Language.json`'dan okunur. Yeni dil eklenirse klasöre `{CultureName}.json` açılır ve bütün anahtarlar çevrilir.
|
||||||
|
(4) Anahtarı silen iş bütün dillerin satırını siler. (5) Klasör dil başına tek dosyadır: dosya adı dilin
|
||||||
|
`CultureName`'i (`tr.json`), `Rows` o dilin bütün satırları; satır başına dosya açılmaz (gruplu klasör, §9.15). (6) Eksik dil bırakılmaz; son mesajda yazılan anahtar ve dil sayısı tek satırla bildirilir.
|
||||||
|
|
||||||
### 4.5 Güncelleme, idempotency ve silme semantiği
|
### 4.5 Güncelleme, idempotency ve silme semantiği
|
||||||
|
|
||||||
**Seeder idempotenttir — bir ekran zaten kuruluysa ve bekleyen kurulumu (`Pending`) yoksa dosya
|
**Seeder idempotenttir — bir ekran zaten kuruluysa ve bekleyen kurulumu (`Pending`) yoksa dosya
|
||||||
|
|
@ -2292,7 +2310,7 @@ geçer. Bu kaplar rapora yalnızca silindiklerinde girer.
|
||||||
|
|
||||||
Kurulu wizard'da silinen ya da veritabanında bulunmayan menü, modül, yetki, yetki grubu ve dil
|
Kurulu wizard'da silinen ya da veritabanında bulunmayan menü, modül, yetki, yetki grubu ve dil
|
||||||
metni satırları, veritabanı işlemi tamamlandıktan sonra seed dosyalarından da çıkarılır (`Sas.Menus.{Menu,Module}.json`,
|
metni satırları, veritabanı işlemi tamamlandıktan sonra seed dosyalarından da çıkarılır (`Sas.Menus.{Menu,Module}.json`,
|
||||||
`Abp.Identity.Permission{s,Groups}.json`, `Sas.Languages.LanguageText.json`;
|
`Abp.Identity.Permission{s,Groups}.json`, `Sas.Languages.LanguageText/{CultureName}.json`;
|
||||||
`IListFormSeedDataSynchronizer.RemoveRowsByFieldAsync`, iş anahtarıyla, `SeedSyncDelete`'e bağlı);
|
`IListFormSeedDataSynchronizer.RemoveRowsByFieldAsync`, iş anahtarıyla, `SeedSyncDelete`'e bağlı);
|
||||||
dosyada kalırsa migrate kaydı geri getirir. Kullanımda olduğu için bırakılan kaydın satırı kalır.
|
dosyada kalırsa migrate kaydı geri getirir. Kullanımda olduğu için bırakılan kaydın satırı kalır.
|
||||||
|
|
||||||
|
|
@ -3569,7 +3587,7 @@ Kurallar:
|
||||||
|
|
||||||
- `export default {ComponentName}` zorunludur; ad dosya/kayıt adıyla birebir aynı olmalıdır.
|
- `export default {ComponentName}` zorunludur; ad dosya/kayıt adıyla birebir aynı olmalıdır.
|
||||||
- Kullanıcıya görünen metni gömme; `translate('{Modül}.…')` kullan ve dil anahtarını wizard
|
- Kullanıcıya görünen metni gömme; `translate('{Modül}.…')` kullan ve dil anahtarını wizard
|
||||||
dosyasında ya da `data/Sas.Languages.LanguageText.json`'da tanımla.
|
dosyasında ya da `data/Sas.Languages.LanguageText/{CultureName}.json`'da tanımla.
|
||||||
- Veriye erişimde `apiService` kullan; taban adres, token ve tenant başlığı ondan gelir.
|
- Veriye erişimde `apiService` kullan; taban adres, token ve tenant başlığı ondan gelir.
|
||||||
Örneklerde gördüğün `axios.create({ baseURL: 'https://localhost:44344/' })` kalıbı **eski bir
|
Örneklerde gördüğün `axios.create({ baseURL: 'https://localhost:44344/' })` kalıbı **eski bir
|
||||||
kayıttır, taklit etme**.
|
kayıttır, taklit etme**.
|
||||||
|
|
@ -4407,7 +4425,7 @@ türeyen `Sas.ListForm.Wizard*`, ABP'nin `Abp.Identity.*` mesajları). Bu ailele
|
||||||
elle silinen alt formun kayıtları üst wizard'ın `Pending.Replaced`'ına yazılır.
|
elle silinen alt formun kayıtları üst wizard'ın `Pending.Replaced`'ına yazılır.
|
||||||
- Kurulu ekranın sahip olduğu dil anahtarı: wizard'ın `Language.Texts` ve `InsertedRecords.LanguageKeys`
|
- Kurulu ekranın sahip olduğu dil anahtarı: wizard'ın `Language.Texts` ve `InsertedRecords.LanguageKeys`
|
||||||
listesinden çıkarılır, `Pending.Replaced.LanguageKeys`'e yazılır (migrate veritabanı satırını siler),
|
listesinden çıkarılır, `Pending.Replaced.LanguageKeys`'e yazılır (migrate veritabanı satırını siler),
|
||||||
EN + TR satırı `data/Sas.Languages.LanguageText.json`'dan silinir.
|
EN + TR satırı ve diğer dillerin satırları `data/Sas.Languages.LanguageText/{CultureName}.json`'dan silinir.
|
||||||
|
|
||||||
Tarama sonunda iki sağlayıcının nesne listesi aynı olmalı (her `sql/` dosyasının `postgres/` eşi var)
|
Tarama sonunda iki sağlayıcının nesne listesi aynı olmalı (her `sql/` dosyasının `postgres/` eşi var)
|
||||||
ve kurulu veritabanında seed dosyası olmayan yordam/view/fonksiyon kalmamalıdır.
|
ve kurulu veritabanında seed dosyası olmayan yordam/view/fonksiyon kalmamalıdır.
|
||||||
|
|
@ -4890,7 +4908,7 @@ elle düzeltme ve wizard'ın kapsamadığı düzenlemeler içindir.
|
||||||
yazılmaz.
|
yazılmaz.
|
||||||
3. **Menü sırası** için wizard'ın `MenuOrder` alanını kullan. Menu Manager'daki taşıma/düzenleme de
|
3. **Menü sırası** için wizard'ın `MenuOrder` alanını kullan. Menu Manager'daki taşıma/düzenleme de
|
||||||
`Sas.Menus.Menu` ekranının seed dosyasına (`data/Sas.Menus.Menu.json`) ve menü adı
|
`Sas.Menus.Menu` ekranının seed dosyasına (`data/Sas.Menus.Menu.json`) ve menü adı
|
||||||
`Sas.Languages.LanguageText.json`'a yansır (`MenuAppService` → `IListFormSeedDataSynchronizer`).
|
`Sas.Languages.LanguageText/{CultureName}.json`'a yansır (`MenuAppService` → `IListFormSeedDataSynchronizer`).
|
||||||
4. **Rol/dil bazlı menü varyantı** gerekiyorsa Menu List ekranından ikinci bir kayıt açılır —
|
4. **Rol/dil bazlı menü varyantı** gerekiyorsa Menu List ekranından ikinci bir kayıt açılır —
|
||||||
aynı `Url`, farklı `RoleId`/`CultureName`. Wizard bunu üretmez.
|
aynı `Url`, farklı `RoleId`/`CultureName`. Wizard bunu üretmez.
|
||||||
|
|
||||||
|
|
@ -5245,13 +5263,19 @@ Ekrandan girilen kayıtların (widget tanımı, parametre tablosu, sabit liste)
|
||||||
sıfırlandığında geri gelmesi için kullanılır. Bağlantı ListForm kaydındaki `SeedFilePath`
|
sıfırlandığında geri gelmesi için kullanılır. Bağlantı ListForm kaydındaki `SeedFilePath`
|
||||||
alanıdır; boşsa ne geri yükleme ne de senkronizasyon yapılır.
|
alanıdır; boşsa ne geri yükleme ne de senkronizasyon yapılır.
|
||||||
|
|
||||||
**Hedef dosya ya da klasördür.** Klasör modunda da her dosya aynı zarfı (`ListFormCode`,
|
**Hedef dosya, klasör ya da gruplu klasördür.** Klasör modlarında da her dosya aynı zarfı (`ListFormCode`,
|
||||||
`KeyFieldName`, `GeneratedAt`, `Order`, `Rows`) taşır; `Rows` tek satırdır ve dosya adı
|
`KeyFieldName`, `GeneratedAt`, `Order`, `Rows`) taşır. Biçimi yalnızca `SeedFilePath` belirler; diskte ne
|
||||||
`{anahtar}.json`'dur. Biçimi yalnızca `SeedFilePath` belirler; diskte ne bulunduğu kararı
|
bulunduğu kararı değiştirmez:
|
||||||
değiştirmez. `.json` ile bitiyorsa bütün satırlar tek dosyadadır (varsayılan). Uzantısız
|
|
||||||
yazılırsa hedef klasördür (`data/{ListFormCode}`) ve her satır `{anahtar}.json` olarak kendi
|
| `SeedFilePath` | Biçim | Kullanım |
|
||||||
dosyasında durur; klasör yoksa ilk kayıtta oluşturulur. Klasör modu satırı büyük ya da sık yazılan
|
| --- | --- | --- |
|
||||||
ekranlar içindir (`SeedPathResolver.GetDataFolderPath`). Hedefi okuyan ve yazan her yol
|
| `data/{ListFormCode}.json` | Tek dosya, bütün satırlar `Rows`'ta (varsayılan) | Küçük listeler |
|
||||||
|
| `data/{ListFormCode}` (uzantısız) | Klasör; her satır `{anahtar}.json`, `Rows` tek satır (`SeedPathResolver.GetDataFolderPath`) | Satırı büyük ya da sık yazılan ekran (Custom Component) |
|
||||||
|
| `data/{ListFormCode}/{Alan}.json` | Gruplu klasör; aynı `{Alan}` değerini taşıyan satırlar `{değer}.json`'da (`SeedPathResolver.GetDataGroupedFolderPath`) | Satır başına dosyanın on binlerce dosya, tek dosyanın her kayıtta onlarca MB yazım üreteceği ekran (dil metinleri: `{CultureName}` → `tr.json`) |
|
||||||
|
|
||||||
|
Klasör yoksa ilk kayıtta oluşturulur. Gruplu klasörde insert/update satırı yalnızca kendi grup dosyasına
|
||||||
|
yazar (grup alanı değiştiyse eski dosyadan çıkarır), delete yalnızca anahtarı taşıyan dosyaya dokunur,
|
||||||
|
satırı kalmayan grup dosyası silinir, yeni grup değeri kendi dosyasını açar; grup değeri boş satır yazılmaz. Hedefi okuyan ve yazan her yol
|
||||||
`ListFormSeedTarget`'ı kullanır. Uygulayıcı `data/` altını özyinelemeli tarar ve bir
|
`ListFormSeedTarget`'ı kullanır. Uygulayıcı `data/` altını özyinelemeli tarar ve bir
|
||||||
klasörün tamamını **tek unit of work'te**, ekran tanımını bir kez çözerek uygular; dosyadaki
|
klasörün tamamını **tek unit of work'te**, ekran tanımını bir kez çözerek uygular; dosyadaki
|
||||||
`ListFormCode` boşsa ekran kodu klasör adından gelir. Ekranın tanımladığı hedefin dışında kalan
|
`ListFormCode` boşsa ekran kodu klasör adından gelir. Ekranın tanımladığı hedefin dışında kalan
|
||||||
|
|
@ -5418,7 +5442,7 @@ isteniyorsa geçilir.
|
||||||
talep sahiplerine kök + `.Create` yeter.
|
talep sahiplerine kök + `.Create` yeter.
|
||||||
7. **Dil** — menü/başlık/açıklama metinleri wizard dosyasındaki `Language` bölümünden,
|
7. **Dil** — menü/başlık/açıklama metinleri wizard dosyasındaki `Language` bölümünden,
|
||||||
alan başlıkları her `Items` öğesinin `En`/`Tr` alanlarından üretilir.
|
alan başlıkları her `Items` öğesinin `En`/`Tr` alanlarından üretilir.
|
||||||
Ayrıca `data/Sas.Languages.LanguageText.json`'a dokunmaya gerek yoktur.
|
Ayrıca `data/Sas.Languages.LanguageText/{CultureName}.json`'a dokunmaya gerek yoktur.
|
||||||
8. **Teslim** — dosyaları listele, DB Migrate gerektiğini söyle, §11 kontrol listesini geç.
|
8. **Teslim** — dosyaları listele, DB Migrate gerektiğini söyle, §11 kontrol listesini geç.
|
||||||
|
|
||||||
**Bu iskeletin türevleri:** izin talebi (tutar yerine gün sayısı, `days` tarifi), masraf talebi
|
**Bu iskeletin türevleri:** izin talebi (tutar yerine gün sayısı, `days` tarifi), masraf talebi
|
||||||
|
|
@ -5434,6 +5458,7 @@ tedarikçi lookup'ı + iki kademeli onay). Kalem taşıyan türevlerde ana ekran
|
||||||
- [ ] Kök yetki + aksiyon yetkileri var; menü `RequiredPermissionName` ile bağlı.
|
- [ ] Kök yetki + aksiyon yetkileri var; menü `RequiredPermissionName` ile bağlı.
|
||||||
- [ ] Yetki grubunun EN + TR görünen adı var.
|
- [ ] Yetki grubunun EN + TR görünen adı var.
|
||||||
- [ ] Kullanıcıya görünen her metnin EN + TR dil anahtarı var; anahtar tekrar edilmemiş.
|
- [ ] Kullanıcıya görünen her metnin EN + TR dil anahtarı var; anahtar tekrar edilmemiş.
|
||||||
|
- [ ] Her yeni anahtar `Sas.Languages.LanguageText/` klasöründeki bütün dil dosyalarında çevrilmiş satırla var (§4.4.3).
|
||||||
- [ ] `IsTenant` (ve gerekiyorsa `IsBranch`/`IsOrganizationUnit`) doğru; sorgular tenant güvenli.
|
- [ ] `IsTenant` (ve gerekiyorsa `IsBranch`/`IsOrganizationUnit`) doğru; sorgular tenant güvenli.
|
||||||
- [ ] Tüm SQL parametreli; string birleştirme yok.
|
- [ ] Tüm SQL parametreli; string birleştirme yok.
|
||||||
- [ ] `DeleteCommand = DefaultDeleteCommand("{Tablo}")`.
|
- [ ] `DeleteCommand = DefaultDeleteCommand("{Tablo}")`.
|
||||||
|
|
|
||||||
52
CLAUDE.md
52
CLAUDE.md
|
|
@ -149,7 +149,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
3. **Yeni ekran/menü** → yetki sözleşmesi olmadan önerilmez. Menü ve route aynı ekran
|
3. **Yeni ekran/menü** → yetki sözleşmesi olmadan önerilmez. Menü ve route aynı ekran
|
||||||
sözleşmesini göstermeli.
|
sözleşmesini göstermeli.
|
||||||
4. **Kullanıcıya görünen her metin** → dil anahtarı üzerinden, EN + TR birlikte. Koda gömülü
|
4. **Kullanıcıya görünen her metin** → dil anahtarı üzerinden, EN + TR birlikte. Koda gömülü
|
||||||
metin önerme.
|
metin önerme. Wizard, MCP ya da herhangi bir bileşen/ekran/menü eklemesinin yazdığı **her** anahtar
|
||||||
|
`Sas.Languages.LanguageText/` klasöründeki bütün dil dosyalarına da yazılır (standing default 27).
|
||||||
**Kullanıcıya görünen her tarih ve sayı** → aktif kültürle: UI'da `localeFormat.ts`
|
**Kullanıcıya görünen her tarih ve sayı** → aktif kültürle: UI'da `localeFormat.ts`
|
||||||
(`formatLocaleDate/Time/DateTime/Number/Currency/Fixed`), API'de `CultureFormat` + `CultureInfo.CurrentCulture`;
|
(`formatLocaleDate/Time/DateTime/Number/Currency/Fixed`), API'de `CultureFormat` + `CultureInfo.CurrentCulture`;
|
||||||
DevExtreme kolon/editörü adlı biçim kullanır (`shortDate`, `shortDateShortTime`, `shortTime`, `decimal`,
|
DevExtreme kolon/editörü adlı biçim kullanır (`shortDate`, `shortDateShortTime`, `shortTime`, `decimal`,
|
||||||
|
|
@ -299,6 +300,23 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
`SelectFieldsDefaultValueJson` çalışma anında okunmaz; okuma koşulu `DefaultFilter`'a yazılır. Ayrıntı:
|
`SelectFieldsDefaultValueJson` çalışma anında okunmaz; okuma koşulu `DefaultFilter`'a yazılır. Ayrıntı:
|
||||||
`lowcode.instructions.md` §4.3.5.3.
|
`lowcode.instructions.md` §4.3.5.3.
|
||||||
|
|
||||||
|
27. **Çok dilli metin: her anahtar bütün dil dosyalarında** → Wizard (`deploy_wizard`, wizard ekranı), MCP (`save_component`,
|
||||||
|
`save_report_*`, `create_table` dil satırları, satır araçları), Custom Component/Visual Designer, menü, yetki grubu,
|
||||||
|
widget, sabit lookup seçeneği (`Sas.StaticLookup.*`) ya da elle yeni ekran — dil anahtarı üreten her iş şunu yapar:
|
||||||
|
1. Anahtarın `LanguageKey`'i ve **EN + TR** metni üretilen yolda yazılır (deploy `Language.Texts` / `Language.*En|Tr`,
|
||||||
|
seeder, elle satır); metni EN + TR veren kullanıcıdır ya da analizdir.
|
||||||
|
2. Aynı işte `Sas.Languages.Language.json`'daki **diğer her dil** (şu an `ar, de-DE, es, fi, fr, hi, hr, hu, it, pt-BR,
|
||||||
|
ru, sk, sl, zh-Hans`) için aynı `ResourceName` + `Key` ile `CultureName` = dil kodu olan satır, o dilin dosyasında
|
||||||
|
(`data/Sas.Languages.LanguageText/{CultureName}.json`, dosya adı dilin `CultureName`'i) çeviriyle yazılır; sabit liste yoktur, dil listesi
|
||||||
|
`Sas.Languages.Language.json`'dan okunur. Yer tutucular (`{0}`), HTML ve kod aynen korunur. `Id` =
|
||||||
|
`Sas_H_LanguageText:{ResourceName}:{Key}:{CultureName}` v5 GUID.
|
||||||
|
3. MCP'de EN + TR satırını deploy yazar; diğer dillerin satırları aynı çağrı dizisinde `Sas.Languages.LanguageText` ekranına
|
||||||
|
`insert_row` ile (her dil için bir satır; `CultureName`, `ResourceName`, `Key`, `Value`) eklenir. Claude Code'da
|
||||||
|
dil dosyalarına doğrudan yazılır.
|
||||||
|
4. Anahtarı silen iş (ekran/wizard silme, bileşen silme) bütün dillerin satırını da siler.
|
||||||
|
5. Son mesajda hangi anahtarların kaç dile yazıldığı tek satırla söylenir; çevirisi eksik dil bırakılmaz.
|
||||||
|
Ayrıntı: `lowcode.instructions.md` §4.4.3.
|
||||||
|
|
||||||
### Cross-cutting: touching one thing means touching these too
|
### Cross-cutting: touching one thing means touching these too
|
||||||
|
|
||||||
- **Modül dokümanı** → `ModuleDocManager.RefreshAllAsync` wizard'ı yazan her yoldan çağrılır (deploy,
|
- **Modül dokümanı** → `ModuleDocManager.RefreshAllAsync` wizard'ı yazan her yoldan çağrılır (deploy,
|
||||||
|
|
@ -322,6 +340,11 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
ayrıdır ve ikisi de kalır: yordam satır kilidi (`UPDLOCK`/`FOR UPDATE`), C# karşılaştır-ve-yaz
|
ayrıdır ve ikisi de kalır: yordam satır kilidi (`UPDLOCK`/`FOR UPDATE`), C# karşılaştır-ve-yaz
|
||||||
(`ISequenceRepository.TryAdvanceAsync`, `SequenceManager` yeniden dener) — entity izlenerek okunup `UpdateAsync`
|
(`ISequenceRepository.TryAdvanceAsync`, `SequenceManager` yeniden dener) — entity izlenerek okunup `UpdateAsync`
|
||||||
ile yazılmaz, iki form aynı numarayı alırdı.
|
ile yazılmaz, iki form aynı numarayı alırdı.
|
||||||
|
- **`LanguageTextInsertCommand` (ListFormSeeder_Sas) ↔ `LanguageTextSeedBulkInserter`** — dil metninin ekleme kuralı
|
||||||
|
(eksik `LanguageKey`'i metnin `Id`'siyle üretmek, aynı kültür + kaynak + anahtar varsa eklememek) iki yerde yazılır:
|
||||||
|
ekrandan gelen kayıt komuttan, migrate'teki seed satırları toplu yazıcıdan geçer (112 bin satır satır başına ~115 sn,
|
||||||
|
toplu ~2 sn). Biri değişirse diğeri de değişir. Satır sayısı büyüyen başka bir ekran aynı yolu
|
||||||
|
`IListFormSeedBulkInserter` ile alır.
|
||||||
- **Liste popup'ı (Grid/Tree) ↔ form sayfası (`FormDevExpress`)** — editör yetenekleri ortak yardımcılardadır
|
- **Liste popup'ı (Grid/Tree) ↔ form sayfası (`FormDevExpress`)** — editör yetenekleri ortak yardımcılardadır
|
||||||
(`list/shared/editingForm.ts`, `editingFormScripts.ts`, `cascade.ts`); TagBox/GridBox/ImageUpload/ImageViewer/Rating'in
|
(`list/shared/editingForm.ts`, `editingFormScripts.ts`, `cascade.ts`); TagBox/GridBox/ImageUpload/ImageViewer/Rating'in
|
||||||
tek uygulaması `form/editors/*`, liste hücresi yalnızca `list/editors/cellEditors.tsx` adaptörüdür. Bir tarafa
|
tek uygulaması `form/editors/*`, liste hücresi yalnızca `list/editors/cellEditors.tsx` adaptörüdür. Bir tarafa
|
||||||
|
|
@ -367,7 +390,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
`IListFormArtifact` / `IListFormFieldArtifact` sözleşmeleri üzerinden yapılır.
|
`IListFormArtifact` / `IListFormFieldArtifact` sözleşmeleri üzerinden yapılır.
|
||||||
Ayrıntı: `dotnet.instructions.md` §5.
|
Ayrıntı: `dotnet.instructions.md` §5.
|
||||||
- **Yeni bir yetki** → `PlatformConsts.AppCodes` + `data/Abp.Identity.Permissions.json` + (UI'da kullanılacaksa)
|
- **Yeni bir yetki** → `PlatformConsts.AppCodes` + `data/Abp.Identity.Permissions.json` + (UI'da kullanılacaksa)
|
||||||
`permission.constant.ts` + `data/Sas.Languages.LanguageText.json`. Yetki ekranındaki sıra `AbpPermissions.Order`
|
`permission.constant.ts` + `data/Sas.Languages.LanguageText/{CultureName}.json`. Yetki ekranındaki sıra `AbpPermissions.Order`
|
||||||
(grup için `AbpPermissionGroups.Order`) sütunundan gelir ve seed satırındaki `Order` alanıyla
|
(grup için `AbpPermissionGroups.Order`) sütunundan gelir ve seed satırındaki `Order` alanıyla
|
||||||
yazılır; sıra kardeşler arasındadır — her üst yetkinin altında 1'den başlar, yeni satırı menüdeki
|
yazılır; sıra kardeşler arasındadır — her üst yetkinin altında 1'den başlar, yeni satırı menüdeki
|
||||||
yerine denk gelen numarayla ekle.
|
yerine denk gelen numarayla ekle.
|
||||||
|
|
@ -414,7 +437,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
- **Wizard deploy veritabanına yazmaz** → wizard ekranı, MCP `deploy_wizard` ve düzenleme aynı
|
- **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`
|
yoldan (`ListFormWizardAppService.Create`) geçer ve **yalnızca seed dosyası yazar**: `wizard/{Ad}.json`
|
||||||
+ menü/modül/izin grubu/yetki/dil satırları (`data/Sas.Menus.{Menu,Module}.json`,
|
+ menü/modül/izin grubu/yetki/dil satırları (`data/Sas.Menus.{Menu,Module}.json`,
|
||||||
`data/Abp.Identity.Permission{Groups,s}.json`, `data/Sas.Languages.LanguageText.json`;
|
`data/Abp.Identity.Permission{Groups,s}.json`, `data/Sas.Languages.LanguageText/{CultureName}.json`;
|
||||||
`WizardSeedRowStore`, anahtarlar `WizardSeedRowKeys`). Kurulum yalnızca DB Migrate'tedir:
|
`WizardSeedRowStore`, anahtarlar `WizardSeedRowKeys`). Kurulum yalnızca DB Migrate'tedir:
|
||||||
`WizardDataSeeder` ekrana özgü kayıtları (veri kaynağı, ListForm, alan, iş akışı) kurar,
|
`WizardDataSeeder` ekrana özgü kayıtları (veri kaynağı, ListForm, alan, iş akışı) kurar,
|
||||||
`ListFormSeedDataApplier` satırları ekler. Deploy/import dosyaya `Pending` yazar; Wizard Manager
|
`ListFormSeedDataApplier` satırları ekler. Deploy/import dosyaya `Pending` yazar; Wizard Manager
|
||||||
|
|
@ -533,9 +556,16 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
yazıları) `SeoAppService` içindedir; yeni bir public kaynak türü eklenirse toplayıcıya da
|
yazıları) `SeoAppService` içindedir; yeni bir public kaynak türü eklenirse toplayıcıya da
|
||||||
eklenir, yoksa sayfa sitemap'te görünmez. Yeni bir meta alanı = iki entity + `SeoRouteDto` +
|
eklenir, yoksa sayfa sitemap'te görünmez. Yeni bir meta alanı = iki entity + `SeoRouteDto` +
|
||||||
`SeoFields` (seeder) + `SeoHead` birlikte.
|
`SeoFields` (seeder) + `SeoHead` birlikte.
|
||||||
- **Yeni bir dil anahtarı** → `configs/seeds/data/Sas.Languages.LanguageText.json` (EN + TR iki satır;
|
- **Yeni bir dil anahtarı** → `configs/seeds/data/Sas.Languages.LanguageText/` klasörünün **bütün** dil dosyalarına
|
||||||
`Id` = `Sas_H_LanguageText:{ResourceName}:{Key}:{CultureName}` adından URL isim alanlı v5 GUID); eklemeden önce anahtarın zaten var olup
|
(`ar, de-DE, en, es, fi, fr, hi, hr, hu, it, pt-BR, ru, sk, sl, tr, zh-Hans` — `Sas.Languages.Language.json`'daki
|
||||||
olmadığını kontrol et.
|
her dil bir dosyadır; **dosya adı dilin `CultureName`'idir** (`{CultureName}.json`: `tr.json`, `de-DE.json`), dosya
|
||||||
|
o dilin bütün satırlarını `Rows` içinde taşır), dil dosyasına anahtar başına bir satır; EN + TR yazılan metindir,
|
||||||
|
diğer 14 dil bu iki metinden çevrilir. Yeni bir dil `Sas.Languages.Language.json`'a eklenirse klasöre
|
||||||
|
`{CultureName}.json` açılır ve bütün anahtarlar çevrilir. Ekranın seed hedefi gruplu klasördür
|
||||||
|
(`SeedFilePath` = `data/Sas.Languages.LanguageText/{CultureName}.json`): ekrandan insert/update/delete yalnızca
|
||||||
|
satırın kültür dosyasına yazar, satır başına dosya açılmaz.
|
||||||
|
`Id` = `Sas_H_LanguageText:{ResourceName}:{Key}:{CultureName}` adından URL isim alanlı v5 GUID; eklemeden önce anahtarın
|
||||||
|
zaten var olup olmadığını kontrol et.
|
||||||
- **Custom component sınıfları** → bileşen kodu veritabanında yaşar, Tailwind derlemede yalnızca
|
- **Custom component sınıfları** → bileşen kodu veritabanında yaşar, Tailwind derlemede yalnızca
|
||||||
seed dosyasını görür. Seed dosyası `ui/` dışında olduğu için aynı sınıflar `ui/safelist.txt`
|
seed dosyasını görür. Seed dosyası `ui/` dışında olduğu için aynı sınıflar `ui/safelist.txt`
|
||||||
dosyasına da yazılır (`ui/twSafelistGenerator.ts` Vite eklentisi, derlemede otomatik); yoksa
|
dosyasına da yazılır (`ui/twSafelistGenerator.ts` Vite eklentisi, derlemede otomatik); yoksa
|
||||||
|
|
@ -592,11 +622,13 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
- **`ListForm.SeedFilePath` dolu bir ekran** → `SeedSyncInsert/SeedSyncUpdate/SeedSyncDelete`
|
- **`ListForm.SeedFilePath` dolu bir ekran** → `SeedSyncInsert/SeedSyncUpdate/SeedSyncDelete`
|
||||||
bayraklarından işaretli olan işlemler seed klasöründeki hedefe yansır
|
bayraklarından işaretli olan işlemler seed klasöründeki hedefe yansır
|
||||||
(`ListFormSeedDataSynchronizer`); yalnızca host bağlamındaki kayıt yansır, kiracının kaydı
|
(`ListFormSeedDataSynchronizer`); yalnızca host bağlamındaki kayıt yansır, kiracının kaydı
|
||||||
veritabanında kalır. Hedef iki biçimdedir ve ayrım uzantıdadır: `.json` ile biten
|
veritabanında kalır. Hedef üç biçimdedir ve ayrım yoldadır: `.json` ile biten
|
||||||
değer **tek dosyadır** (`data/{ListFormCode}.json`, bütün satırlar orada), uzantısız değer
|
değer **tek dosyadır** (`data/{ListFormCode}.json`, bütün satırlar orada), uzantısız değer
|
||||||
**klasördür** (`data/{ListFormCode}/{anahtar}.json`, her satır kendi dosyasında) ve klasör yoksa
|
**klasördür** (`data/{ListFormCode}/{anahtar}.json`, her satır kendi dosyasında), `{Alan}.json` ile
|
||||||
ilk kayıtta oluşturulur. Klasör modu satırı büyük ya da sık yazılan ekranlar içindir
|
biten değer **gruplu klasördür** (`data/{ListFormCode}/{Alan}.json` → aynı alan değerini taşıyan satırlar
|
||||||
(varsayılan tek dosya). **Biçimin tek kaynağı ekranın tanımıdır**; diskte ne bulunduğu kararı
|
`{değer}.json`'da; dil metinleri `{CultureName}`) ve klasör yoksa ilk kayıtta oluşturulur. Klasör modu
|
||||||
|
satırı büyük ya da sık yazılan, gruplu mod satır başına dosyanın on binlerce dosya üreteceği ekranlar
|
||||||
|
içindir (varsayılan tek dosya). **Biçimin tek kaynağı ekranın tanımıdır**; diskte ne bulunduğu kararı
|
||||||
değiştirmez ve tanım kod içinde ekran adıyla sabitlenmez. Hedefi okuyan/yazan her yol
|
değiştirmez ve tanım kod içinde ekran adıyla sabitlenmez. Hedefi okuyan/yazan her yol
|
||||||
(senkronizasyon, export/import, temizleme, wizard satır deposu, MCP demo satırları, bildirim
|
(senkronizasyon, export/import, temizleme, wizard satır deposu, MCP demo satırları, bildirim
|
||||||
varsayılanları) `ListFormSeedTarget` üzerinden gider; paylaşılan ekranların tanımı
|
varsayılanları) `ListFormSeedTarget` üzerinden gider; paylaşılan ekranların tanımı
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -308,7 +308,7 @@ yazılır, PostgreSQL'de `PostgreSqlModelSyntax` ile çevrilir (`PlatformDbConte
|
||||||
| `{sql\|postgres}/host/{object\|execute}` | Aynı iki kural, **yalnızca host veritabanında**. Tenant veritabanında karşılığı olmayan nesnelere dayanan script buraya konur — örneğin HangFire tabloları tenant migrate'inde de yalnızca host'a kurulduğu için `Sas_H_BackgroundWorker_JobFlow` view'ı buradadır. Sıra: `object` → `host/object` → `execute` → `host/execute`. SqlTableDesigner ve SqlQueryManager her zaman `object/`'e yazar; yalnızca host'ta çalışması gereken script File Manager'dan buraya taşınır. Wizard export/import bu klasörü de taşır. |
|
| `{sql\|postgres}/host/{object\|execute}` | Aynı iki kural, **yalnızca host veritabanında**. Tenant veritabanında karşılığı olmayan nesnelere dayanan script buraya konur — örneğin HangFire tabloları tenant migrate'inde de yalnızca host'a kurulduğu için `Sas_H_BackgroundWorker_JobFlow` view'ı buradadır. Sıra: `object` → `host/object` → `execute` → `host/execute`. SqlTableDesigner ve SqlQueryManager her zaman `object/`'e yazar; yalnızca host'ta çalışması gereken script File Manager'dan buraya taşınır. Wizard export/import bu klasörü de taşır. |
|
||||||
| `wizard` | ListForm Wizard'ın ürettiği `.json` konfigürasyonları; `WizardDataSeeder` okur. Contributor değildir (`IWizardDataSeeder`): her dosyayı kendi transaction'ında kurduğu için ABP contributor'ları commit edildikten sonra, `data/` dosyalarından önce çalışır — açık contributor transaction'ı içinde ayrı bağlantı, commit edilmemiş satırları bekleyip zaman aşımına düşer. |
|
| `wizard` | ListForm Wizard'ın ürettiği `.json` konfigürasyonları; `WizardDataSeeder` okur. Contributor değildir (`IWizardDataSeeder`): her dosyayı kendi transaction'ında kurduğu için ABP contributor'ları commit edildikten sonra, `data/` dosyalarından önce çalışır — açık contributor transaction'ı içinde ayrı bağlantı, commit edilmemiş satırları bekleyip zaman aşımına düşer. |
|
||||||
| `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` dosyaları `ICrudEndpointSeedApplier` üzerinden uygular. Politika **damga karşılaştırmalıdır**: eksik endpoint her zaman eklenir, mevcut bir kayıt yalnızca dosyanın `GeneratedAt` damgası kaydın `LastModificationTime`/`CreationTime` değerinden **yeniyse** güncellenir ya da silinir; aksi halde kayıt korunur ve log'a drift uyarısı düşer. Böylece CRUD Endpoint Manager'dan yapılan bir revizyon depodan gelen eski bir dosya yüzünden migrate sırasında geri alınmaz. Damgası olmayan (eski biçimde yazılmış) dosya hiçbir zaman yeni sayılmaz. |
|
| `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` dosyaları `ICrudEndpointSeedApplier` üzerinden uygular. Politika **damga karşılaştırmalıdır**: eksik endpoint her zaman eklenir, mevcut bir kayıt yalnızca dosyanın `GeneratedAt` damgası kaydın `LastModificationTime`/`CreationTime` değerinden **yeniyse** güncellenir ya da silinir; aksi halde kayıt korunur ve log'a drift uyarısı düşer. Böylece CRUD Endpoint Manager'dan yapılan bir revizyon depodan gelen eski bir dosya yüzünden migrate sırasında geri alınmaz. Damgası olmayan (eski biçimde yazılmış) dosya hiçbir zaman yeni sayılmaz. |
|
||||||
| `data` | Ekranlardan girilen listelerin veri aynası: tek dosya (`{ListFormCode}.json`) ya da satır başına dosya tutan klasör (`{ListFormCode}/{anahtar}.json`; klasör özyinelemeli taranır ve tamamı tek unit of work'te uygulanır). Biçimi yalnızca ekranın `SeedFilePath` tanımı belirler; hedefi okuyan/yazan her yol `ListFormSeedTarget` üzerinden gider ve tanımın dışında kalan dosya uygulanmaz. `ListForm.SeedFilePath` dolu olan ekranda `SeedSyncInsert/Update/Delete` bayraklarından işaretli olan işlemler dosyaya yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında bütün contributor'lar bittikten sonra `ListFormSeedDataApplier` dosyayı geri uygular: anahtarı veritabanında **olmayan** satırları ekler, var olanlara dokunmaz. Aynı uygulayıcı wizard import'unun kapanışında da (`CompleteImport`) yalnızca o import'ta yazılan dosyalar için, `overwriteExisting` açık olarak çalışır — orada var olan satır dosyadaki değerlerle güncellenir. |
|
| `data` | Ekranlardan girilen listelerin veri aynası: tek dosya (`{ListFormCode}.json`) ya da satır başına dosya tutan klasör (`{ListFormCode}/{anahtar}.json`; klasör özyinelemeli taranır ve tamamı tek unit of work'te uygulanır). Biçimi yalnızca ekranın `SeedFilePath` tanımı belirler; hedefi okuyan/yazan her yol `ListFormSeedTarget` üzerinden gider ve tanımın dışında kalan dosya uygulanmaz. `ListForm.SeedFilePath` dolu olan ekranda `SeedSyncInsert/Update/Delete` bayraklarından işaretli olan işlemler dosyaya yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında bütün contributor'lar bittikten sonra `ListFormSeedDataApplier` dosyayı geri uygular: anahtarı veritabanında **olmayan** satırları ekler, var olanlara dokunmaz. Yeni satırlar toplu komutlarla yazılır; satır sayısı büyüyen ekranın `IListFormSeedBulkInserter` yazıcısı varsa (dil metinleri: `LanguageTextSeedBulkInserter`) satırlar ekranın `InsertCommand`'ının kuralları bellekte uygulanarak `IDynamicDataRepository.BulkInsertAsync` ile (SQL Server bulk copy, PostgreSQL binary COPY) yazılır. Aynı uygulayıcı wizard import'unun kapanışında da (`CompleteImport`) yalnızca o import'ta yazılan dosyalar için, `overwriteExisting` açık olarak çalışır — orada var olan satır dosyadaki değerlerle güncellenir. |
|
||||||
|
|
||||||
Klasör kökünde kalmış eski `.sql` dosyaları geriye dönük uyumluluk için hâlâ işlenir,
|
Klasör kökünde kalmış eski `.sql` dosyaları geriye dönük uyumluluk için hâlâ işlenir,
|
||||||
fakat `SqlDataSeeder` bunları uyarı ile loglar — ilgili klasöre taşınmaları beklenir.
|
fakat `SqlDataSeeder` bunları uyarı ile loglar — ilgili klasöre taşınmaları beklenir.
|
||||||
|
|
@ -587,7 +587,7 @@ doldurur, `BuildListForm()/BuildFields()` ile ekranı üretir ve `RefreshFrom` i
|
||||||
`WizardDataSeeder` ekranı aynı iki metotla dosyadan kurar. **Deploy veritabanına yazmaz:**
|
`WizardDataSeeder` ekranı aynı iki metotla dosyadan kurar. **Deploy veritabanına yazmaz:**
|
||||||
wizard dosyasının yanında menü, modül, izin grubu, yetki ve dil metinleri platform seed
|
wizard dosyasının yanında menü, modül, izin grubu, yetki ve dil metinleri platform seed
|
||||||
dosyalarına (`data/Sas.Menus.{Menu,Module}.json`, `data/Abp.Identity.Permission{Groups,s}.json`,
|
dosyalarına (`data/Sas.Menus.{Menu,Module}.json`, `data/Abp.Identity.Permission{Groups,s}.json`,
|
||||||
`data/Sas.Languages.LanguageText.json`) satır olarak yazılır (`WizardSeedRowStore`) ve hepsi DB
|
`data/Sas.Languages.LanguageText/{CultureName}.json`) satır olarak yazılır (`WizardSeedRowStore`) ve hepsi DB
|
||||||
Migrate ile kurulur. Deploy/import dosyaya `Pending` yazar; Wizard Manager dosyayı "Migrate
|
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
|
bekliyor" rozetiyle gösterir, migrate bitince liste ve menü yenilenir. Yetkiler hiçbir role
|
||||||
otomatik atanmaz. Böylece Wizard ile ListForm
|
otomatik atanmaz. Böylece Wizard ile ListForm
|
||||||
|
|
@ -978,7 +978,7 @@ Route.Authority ──┘ └► Permission
|
||||||
- **Menu** (`Menu.cs`): `Code`, `ParentCode`, `DisplayName`, `Url`, `Icon`, `Order`,
|
- **Menu** (`Menu.cs`): `Code`, `ParentCode`, `DisplayName`, `Url`, `Icon`, `Order`,
|
||||||
`RequiredPermissionName`, `Target`, `IsDisabled`, ayrıca `UserId` / `RoleId` / `CultureName`
|
`RequiredPermissionName`, `Target`, `IsDisabled`, ayrıca `UserId` / `RoleId` / `CultureName`
|
||||||
ile kullanıcı, rol ve dil bazlı menü varyantları.
|
ile kullanıcı, rol ve dil bazlı menü varyantları.
|
||||||
- **Menü yöneticisi** (`/admin/menuManager`): sürükle-bırak ağaç ile menü düzenleme. Kayıtlar `Sas.Menus.Menu` ekranıyla aynı seed dosyasına (`data/Sas.Menus.Menu.json`, `SeedSync*` bayraklarına bağlı) yazılır; menü adları `Sas.Languages.LanguageText.json`'a.
|
- **Menü yöneticisi** (`/admin/menuManager`): sürükle-bırak ağaç ile menü düzenleme. Kayıtlar `Sas.Menus.Menu` ekranıyla aynı seed dosyasına (`data/Sas.Menus.Menu.json`, `SeedSync*` bayraklarına bağlı) yazılır; menü adları `Sas.Languages.LanguageText/{CultureName}.json`'a.
|
||||||
- Frontend'de `dynamicRoutesContext` route'ları çeker, `dynamicRouteLoader` bunları fiziksel
|
- Frontend'de `dynamicRoutesContext` route'ları çeker, `dynamicRouteLoader` bunları fiziksel
|
||||||
view'lara veya runtime derlenmiş Custom Component'lere eşler, `DynamicRouter` React Router
|
view'lara veya runtime derlenmiş Custom Component'lere eşler, `DynamicRouter` React Router
|
||||||
ağacını kurar; her korumalı route `ProtectedRoute` + `PermissionGuard` ile sarılır.
|
ağacını kurar; her korumalı route `ProtectedRoute` + `PermissionGuard` ile sarılır.
|
||||||
|
|
@ -1189,7 +1189,7 @@ bileşenler tek bir sözlükte durur (`RUNTIME_NODE_COMPONENTS`): kanvas onu `Vi
|
||||||
tasarım ve çalışma zamanı ayrışamaz. Sözlükte `PlatformIcon`, `BarcodeScanField`
|
tasarım ve çalışma zamanı ayrışamaz. Sözlükte `PlatformIcon`, `BarcodeScanField`
|
||||||
(`components/shared/BarcodeScanner`), `BarcodeViewer` (`components/shared/BarcodeViewer.tsx`) ve
|
(`components/shared/BarcodeScanner`), `BarcodeViewer` (`components/shared/BarcodeViewer.tsx`) ve
|
||||||
public site parçaları (`components/publicSite`) bulunur. Yeni bir düğüm eklerken: bileşen `components/publicSite` (ya da
|
public site parçaları (`components/publicSite`) bulunur. Yeni bir düğüm eklerken: bileşen `components/publicSite` (ya da
|
||||||
uygun klasör) → `runtimeNodes.ts` sözlüğü → `catalog.ts` tanımı (`kind: 'custom'`) → `data/Sas.Languages.LanguageText.json`
|
uygun klasör) → `runtimeNodes.ts` sözlüğü → `catalog.ts` tanımı (`kind: 'custom'`) → `data/Sas.Languages.LanguageText/{CultureName}.json`
|
||||||
açıklama anahtarı.
|
açıklama anahtarı.
|
||||||
|
|
||||||
**Barkod.** Okuma ve çizme ayrı yollardır, ikisi de `utils/barcode.ts` içindedir ve `zxing-wasm`
|
**Barkod.** Okuma ve çizme ayrı yollardır, ikisi de `utils/barcode.ts` içindedir ve `zxing-wasm`
|
||||||
|
|
@ -2239,7 +2239,7 @@ configs/seeds/
|
||||||
│ # Hr.{CandidateSource,RejectionReason,EvaluationCriterion}.json — İK › Tanımlar altındaki işe alım tanımları
|
│ # Hr.{CandidateSource,RejectionReason,EvaluationCriterion}.json — İK › Tanımlar altındaki işe alım tanımları
|
||||||
│ # Hr.PerformanceRating.json — İK › Tanımlar › Performans Dereceleri (5 kademeli ölçek)
|
│ # Hr.PerformanceRating.json — İK › Tanımlar › Performans Dereceleri (5 kademeli ölçek)
|
||||||
│ # Adm.Notifications.Notification{Templates,Types,Rules}.json — bildirim şablonu, tipi → kuralı
|
│ # Adm.Notifications.Notification{Templates,Types,Rules}.json — bildirim şablonu, tipi → kuralı
|
||||||
│ # Sas.Languages.{Language,LanguageText}.json — dil → metin (eksik LanguageKey metinle üretilir)
|
│ # Sas.Languages.Language.json + Sas.Languages.LanguageText/{CultureName}.json — dil → metin (eksik LanguageKey metinle üretilir)
|
||||||
│ # Sas.SettingDefinitions.json, Sas.BackgroundWorkers.RecurringJobs.json — ayar tanımları, zamanlanmış işler
|
│ # Sas.SettingDefinitions.json, Sas.BackgroundWorkers.RecurringJobs.json — ayar tanımları, zamanlanmış işler
|
||||||
├── sql/{object,execute}/ # .sql (SQL Server)
|
├── sql/{object,execute}/ # .sql (SQL Server)
|
||||||
│ └── host/{object,execute}/ # yalnızca host veritabanında çalışır
|
│ └── host/{object,execute}/ # yalnızca host veritabanında çalışır
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Modulun paylasilan sozlesmesi. Kullaniciya gorunen metinler platformun kendi
|
/// Modulun paylasilan sozlesmesi. Kullaniciya gorunen metinler platformun kendi
|
||||||
/// <c>PlatformResource</c> kaynagindan (configs/seeds/data/Sas.Languages.LanguageText.json) okunur; modul ikinci bir dil
|
/// <c>PlatformResource</c> kaynagindan (configs/seeds/data/Sas.Languages.LanguageText/{CultureName}.json) okunur; modul ikinci bir dil
|
||||||
/// kaynagi acmaz.
|
/// kaynagi acmaz.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DependsOn(
|
[DependsOn(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
namespace Sozsoft.Mcp;
|
namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MCP hatalarinin dil anahtarlari. Karsiliklari platformun <c>configs/seeds/data/Sas.Languages.LanguageText.json</c>
|
/// MCP hatalarinin dil anahtarlari. Karsiliklari platformun <c>configs/seeds/data/Sas.Languages.LanguageText/{CultureName}.json</c>
|
||||||
/// dosyasinda EN + TR olarak durur; koda gomulu metin kullanilmaz.
|
/// dosyasinda EN + TR olarak durur; koda gomulu metin kullanilmaz.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class McpErrorCodes
|
public static class McpErrorCodes
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ namespace Sozsoft.Mcp;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MCP islemlerinin talep Timesheets satirina yazilan aciklamalari. Karsiliklari platformun
|
/// MCP islemlerinin talep Timesheets satirina yazilan aciklamalari. Karsiliklari platformun
|
||||||
/// <c>Sas.Languages.LanguageText.json</c> dosyasinda EN + TR olarak durur; satir Turkce yazilir.
|
/// <c>Sas.Languages.LanguageText/{CultureName}.json</c> dosyalarinda (her dilde bir satir) durur; satir Turkce yazilir.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class McpTimesheetTexts
|
public static class McpTimesheetTexts
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Languages.Languages;
|
||||||
using Sozsoft.Platform.Data.Seeds;
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.ListForms;
|
namespace Sozsoft.Platform.ListForms;
|
||||||
|
|
@ -54,7 +55,7 @@ public sealed class ModuleDocCatalog
|
||||||
/// <summary>Menu kodu → ust menu kodu ve sirasi (<c>data/Sas.Menus.Menu.json</c>).</summary>
|
/// <summary>Menu kodu → ust menu kodu ve sirasi (<c>data/Sas.Menus.Menu.json</c>).</summary>
|
||||||
private readonly Dictionary<string, (string? Parent, int Order)> menus = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, (string? Parent, int Order)> menus = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
/// <summary>Menu kodu → Turkce adi (<c>data/Sas.Languages.LanguageText.json</c>); bolum basliklari icin.</summary>
|
/// <summary>Menu kodu → Turkce adi (<c>data/Sas.Languages.LanguageText/tr.json</c>); bolum basliklari icin.</summary>
|
||||||
private readonly Dictionary<string, string> menuTitles = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, string> menuTitles = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private ModuleDocCatalog(string scopeRoot)
|
private ModuleDocCatalog(string scopeRoot)
|
||||||
|
|
@ -121,13 +122,15 @@ public sealed class ModuleDocCatalog
|
||||||
menus[code] = (Text(row, "ParentCode"), row.TryGetProperty("Order", out var order) && order.TryGetInt32(out var value) ? value : int.MaxValue);
|
menus[code] = (Text(row, "ParentCode"), row.TryGetProperty("Order", out var order) && order.TryGetInt32(out var value) ? value : int.MaxValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var row in await ReadRowsAsync("Sas.Languages.LanguageText.json"))
|
// Dil metinleri kultur basina tek dosyadadir (data/Sas.Languages.LanguageText/{CultureName}.json);
|
||||||
|
// yalnizca Turkce dosya okunur.
|
||||||
|
foreach (var row in await ReadRowsAsync($"{PlatformConsts.AppCodes.Languages.LanguageText}/{LanguageCodes.Tr}.json"))
|
||||||
{
|
{
|
||||||
var key = Text(row, "Key");
|
var key = Text(row, "Key");
|
||||||
if (key == null || !menus.ContainsKey(key))
|
if (key == null || !menus.ContainsKey(key))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (string.Equals(Text(row, "CultureName"), "tr", StringComparison.OrdinalIgnoreCase) && Text(row, "Value") is { } title)
|
if (Text(row, "Value") is { } title)
|
||||||
menuTitles[key] = title;
|
menuTitles[key] = title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using Microsoft.Extensions.Options;
|
||||||
using Volo.Abp;
|
using Volo.Abp;
|
||||||
using Volo.Abp.Authorization.Permissions;
|
using Volo.Abp.Authorization.Permissions;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
|
using Volo.Abp.MultiTenancy;
|
||||||
using Volo.Abp.PermissionManagement;
|
using Volo.Abp.PermissionManagement;
|
||||||
using Volo.Abp.SimpleStateChecking;
|
using Volo.Abp.SimpleStateChecking;
|
||||||
|
|
||||||
|
|
@ -32,19 +33,98 @@ public class PlatformPermissionGrantInfoDto : PermissionGrantInfoDto
|
||||||
[RemoteService(false)]
|
[RemoteService(false)]
|
||||||
public class PlatformPermissionAppService : PermissionAppService
|
public class PlatformPermissionAppService : PermissionAppService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Atamasi <c>AbpPermissionGrants</c> satirindan ibaret olan saglayicilar; toplu yazim yalnizca bunlarda
|
||||||
|
/// <c>PermissionManager.SetAsync</c> ile ayni sonucu verir.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly HashSet<string> BulkProviders =
|
||||||
|
[RolePermissionValueProvider.ProviderName, UserPermissionValueProvider.ProviderName];
|
||||||
|
|
||||||
|
private readonly IPermissionGrantRepository permissionGrantRepository;
|
||||||
|
|
||||||
public PlatformPermissionAppService(
|
public PlatformPermissionAppService(
|
||||||
IPermissionManager permissionManager,
|
IPermissionManager permissionManager,
|
||||||
IPermissionDefinitionManager permissionDefinitionManager,
|
IPermissionDefinitionManager permissionDefinitionManager,
|
||||||
IOptions<PermissionManagementOptions> options,
|
IOptions<PermissionManagementOptions> options,
|
||||||
ISimpleStateCheckerManager<PermissionDefinition> simpleStateCheckerManager)
|
ISimpleStateCheckerManager<PermissionDefinition> simpleStateCheckerManager,
|
||||||
|
IPermissionGrantRepository permissionGrantRepository)
|
||||||
: base(permissionManager, permissionDefinitionManager, options, simpleStateCheckerManager)
|
: base(permissionManager, permissionDefinitionManager, options, simpleStateCheckerManager)
|
||||||
{
|
{
|
||||||
|
this.permissionGrantRepository = permissionGrantRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ABP her yetki icin ayri <c>SetAsync</c> cagirir (yetki basina birkac sorgu + insert/delete); "tumunu sec"
|
||||||
|
/// binlerce yetkide dakikalar surer. Mevcut atamalar tek sorguda okunur, fark toplu yazilir; SetAsync'in
|
||||||
|
/// dogrulamalari (tanim, etkinlik, saglayici, kiraci tarafi) korunur. Cache, entity degisim olayiyla
|
||||||
|
/// (PermissionGrantCacheItemInvalidator) her satir icin yine temizlenir.
|
||||||
|
/// </summary>
|
||||||
|
public override async Task UpdateAsync(string providerName, string providerKey, UpdatePermissionsDto input)
|
||||||
|
{
|
||||||
|
if (!BulkProviders.Contains(providerName))
|
||||||
|
{
|
||||||
|
await base.UpdateAsync(providerName, providerKey, input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CheckProviderPolicy(providerName);
|
||||||
|
|
||||||
|
var definitions = (await PermissionDefinitionManager.GetPermissionsAsync()).ToDictionary(d => d.Name);
|
||||||
|
var requested = new Dictionary<string, bool>();
|
||||||
|
foreach (var permission in input.Permissions)
|
||||||
|
{
|
||||||
|
requested[permission.Name] = permission.IsGranted;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestedDefinitions = requested.Keys
|
||||||
|
.Select(name => definitions.GetOrDefault(name)
|
||||||
|
?? throw new AbpException($"Undefined permission: {name}"))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var enabled = await SimpleStateCheckerManager.IsEnabledAsync(requestedDefinitions);
|
||||||
|
var side = CurrentTenant.GetMultiTenancySide();
|
||||||
|
foreach (var definition in requestedDefinitions)
|
||||||
|
{
|
||||||
|
if (!definition.IsEnabled || !enabled[definition])
|
||||||
|
{
|
||||||
|
throw new AbpException($"The permission named {definition.Name} is disabled!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.Providers.Count != 0 && !definition.Providers.Contains(providerName))
|
||||||
|
{
|
||||||
|
throw new AbpException($"The permission named {definition.Name} has not compatible with the provider named {providerName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!definition.MultiTenancySide.HasFlag(side))
|
||||||
|
{
|
||||||
|
throw new AbpException($"The permission named {definition.Name} has multitenancy side {definition.MultiTenancySide} which is not compatible with the current multitenancy side {side}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = await permissionGrantRepository.GetListAsync(providerName, providerKey);
|
||||||
|
var existingNames = existing.Select(grant => grant.Name).ToHashSet();
|
||||||
|
|
||||||
|
var toRevoke = existing.Where(grant => requested.TryGetValue(grant.Name, out var isGranted) && !isGranted).ToList();
|
||||||
|
var toGrant = requested
|
||||||
|
.Where(pair => pair.Value && !existingNames.Contains(pair.Key))
|
||||||
|
.Select(pair => new PermissionGrant(GuidGenerator.Create(), pair.Key, providerName, providerKey, CurrentTenant.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (toRevoke.Count != 0)
|
||||||
|
{
|
||||||
|
await permissionGrantRepository.DeleteManyAsync(toRevoke, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toGrant.Count != 0)
|
||||||
|
{
|
||||||
|
await permissionGrantRepository.InsertManyAsync(toGrant, autoSave: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<GetPermissionListResultDto> GetAsync(string providerName, string providerKey)
|
public override async Task<GetPermissionListResultDto> GetAsync(string providerName, string providerKey)
|
||||||
{
|
{
|
||||||
var baseResult = await base.GetAsync(providerName, providerKey);
|
var baseResult = await base.GetAsync(providerName, providerKey);
|
||||||
var allDefinitions = await PermissionDefinitionManager.GetPermissionsAsync();
|
var allDefinitions = (await PermissionDefinitionManager.GetPermissionsAsync()).ToDictionary(d => d.Name);
|
||||||
var allGroups = await PermissionDefinitionManager.GetGroupsAsync();
|
var allGroups = await PermissionDefinitionManager.GetGroupsAsync();
|
||||||
|
|
||||||
// Yeni tipte result
|
// Yeni tipte result
|
||||||
|
|
@ -65,7 +145,7 @@ public class PlatformPermissionAppService : PermissionAppService
|
||||||
|
|
||||||
foreach (var permission in SortByOrder(group.Permissions, allDefinitions))
|
foreach (var permission in SortByOrder(group.Permissions, allDefinitions))
|
||||||
{
|
{
|
||||||
var definition = allDefinitions.FirstOrDefault(x => x.Name == permission.Name);
|
var definition = allDefinitions.GetOrDefault(permission.Name);
|
||||||
var modules = definition?.Properties.GetOrDefault(PlatformConsts.Permissions.Modules)?.ToString();
|
var modules = definition?.Properties.GetOrDefault(PlatformConsts.Permissions.Modules)?.ToString();
|
||||||
|
|
||||||
var newPermission = new PlatformPermissionGrantInfoDto
|
var newPermission = new PlatformPermissionGrantInfoDto
|
||||||
|
|
@ -99,7 +179,7 @@ public class PlatformPermissionAppService : PermissionAppService
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static List<PermissionGrantInfoDto> SortByOrder(
|
private static List<PermissionGrantInfoDto> SortByOrder(
|
||||||
List<PermissionGrantInfoDto> permissions,
|
List<PermissionGrantInfoDto> permissions,
|
||||||
IReadOnlyList<PermissionDefinition> definitions)
|
Dictionary<string, PermissionDefinition> definitions)
|
||||||
{
|
{
|
||||||
var byName = permissions.ToDictionary(permission => permission.Name!);
|
var byName = permissions.ToDictionary(permission => permission.Name!);
|
||||||
|
|
||||||
|
|
@ -109,23 +189,20 @@ public class PlatformPermissionAppService : PermissionAppService
|
||||||
private static string BuildOrderKey(
|
private static string BuildOrderKey(
|
||||||
PermissionGrantInfoDto permission,
|
PermissionGrantInfoDto permission,
|
||||||
Dictionary<string, PermissionGrantInfoDto> byName,
|
Dictionary<string, PermissionGrantInfoDto> byName,
|
||||||
IReadOnlyList<PermissionDefinition> definitions)
|
Dictionary<string, PermissionDefinition> definitions)
|
||||||
{
|
{
|
||||||
var chain = new List<string>();
|
var chain = new List<string>();
|
||||||
|
|
||||||
// Ust yetki listede yoksa (yetki suzulmusse) zincir orada biter.
|
// Ust yetki listede yoksa (yetki suzulmusse) zincir orada biter.
|
||||||
for (var current = permission; current != null; current = current.ParentName == null ? null : byName.GetOrDefault(current.ParentName))
|
for (var current = permission; current != null; current = current.ParentName == null ? null : byName.GetOrDefault(current.ParentName))
|
||||||
{
|
{
|
||||||
chain.Insert(0, GetOrder(definitions, current.Name).ToString("D6"));
|
chain.Insert(0, GetOrder(definitions.GetOrDefault(current.Name)?.Properties).ToString("D6"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ayirici rakamlardan kucuk bir karakterdir; boylece ebeveynin anahtari cocugunkinin onune duser.
|
// Ayirici rakamlardan kucuk bir karakterdir; boylece ebeveynin anahtari cocugunkinin onune duser.
|
||||||
return string.Join('/', chain);
|
return string.Join('/', chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetOrder(IReadOnlyList<PermissionDefinition> definitions, string? name)
|
|
||||||
=> GetOrder(definitions.FirstOrDefault(definition => definition.Name == name)?.Properties);
|
|
||||||
|
|
||||||
private static int GetOrder(IReadOnlyList<PermissionGroupDefinition> groups, string? name)
|
private static int GetOrder(IReadOnlyList<PermissionGroupDefinition> groups, string? name)
|
||||||
=> GetOrder(groups.FirstOrDefault(group => group.Name == name)?.Properties);
|
=> GetOrder(groups.FirstOrDefault(group => group.Name == name)?.Properties);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Sozsoft.Languages.Entities;
|
||||||
using Sozsoft.Languages.Languages;
|
using Sozsoft.Languages.Languages;
|
||||||
using Sozsoft.Mcp;
|
using Sozsoft.Mcp;
|
||||||
using Sozsoft.Platform.Editors;
|
using Sozsoft.Platform.Editors;
|
||||||
|
|
@ -3468,7 +3469,7 @@ public class ListFormSeeder_Sas : IDataSeedContributor, ITransientDependency
|
||||||
", 3),
|
", 3),
|
||||||
KeyFieldName = "Id",
|
KeyFieldName = "Id",
|
||||||
KeyFieldDbSourceType = DbType.Guid,
|
KeyFieldDbSourceType = DbType.Guid,
|
||||||
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
SeedFilePath = SeedPathResolver.GetDataGroupedFolderPath(listFormName, nameof(LanguageText.CultureName)),
|
||||||
SeedSyncInsert = true,
|
SeedSyncInsert = true,
|
||||||
SeedSyncUpdate = true,
|
SeedSyncUpdate = true,
|
||||||
SeedSyncDelete = true,
|
SeedSyncDelete = true,
|
||||||
|
|
@ -11704,6 +11705,8 @@ public class ListFormSeeder_Sas : IDataSeedContributor, ITransientDependency
|
||||||
/// Metin is anahtari (kultur + kaynak + anahtar) zaten varsa eklenmez — wizard seeder'in ya da
|
/// Metin is anahtari (kultur + kaynak + anahtar) zaten varsa eklenmez — wizard seeder'in ya da
|
||||||
/// baska bir kurulumun farkli kimlikle yazdigi metin seed dosyasi uygulanirken kopyalanmasin.
|
/// baska bir kurulumun farkli kimlikle yazdigi metin seed dosyasi uygulanirken kopyalanmasin.
|
||||||
/// Son select gercek kaydin anahtarini dondurur; senkronizasyon satiri onunla okur.
|
/// Son select gercek kaydin anahtarini dondurur; senkronizasyon satiri onunla okur.
|
||||||
|
/// Migrate'te seed satirlari bu komutla degil ayni kurallari uygulayan <c>LanguageTextSeedBulkInserter</c>
|
||||||
|
/// ile yazilir; kural degisirse ikisi birlikte degisir.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string LanguageTextInsertCommand()
|
private static string LanguageTextInsertCommand()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
107
api/src/Sozsoft.Platform.Domain/Data/ListFormSeedBulkInserter.cs
Normal file
107
api/src/Sozsoft.Platform.Domain/Data/ListFormSeedBulkInserter.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Languages.Entities;
|
||||||
|
using Sozsoft.Platform.DynamicData;
|
||||||
|
using Sozsoft.Platform.Entities;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Data.Seeds;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir ekranin seed dosyasindaki yeni satirlarini, ekranin satir basina <c>InsertCommand</c>'i yerine toplu
|
||||||
|
/// kopyalamayla yazar. Yalnizca satir sayisi buyuyen ve komutu set tabanli yeniden yazilabilen ekranlar
|
||||||
|
/// icindir; yazici komutun yaptigini birebir yapmak zorundadir. Yazicisi olmayan ekran
|
||||||
|
/// <see cref="ListFormSeedDataApplier"/>'in toplu komut yolundan gecer.
|
||||||
|
/// </summary>
|
||||||
|
public interface IListFormSeedBulkInserter
|
||||||
|
{
|
||||||
|
bool CanInsert(ListForm listForm);
|
||||||
|
|
||||||
|
/// <param name="rows">Anahtari tabloda olmayan, varsayilanlari doldurulmus satirlar.</param>
|
||||||
|
Task InsertAsync(
|
||||||
|
IReadOnlyList<Dictionary<string, object>> rows,
|
||||||
|
IDynamicDataRepository repository,
|
||||||
|
string connectionString,
|
||||||
|
DataSourceTypeEnum dataSourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dil metinleri: her kultur her anahtar icin bir satir tasidigi icin yeni bilesenlerle en hizli buyuyen
|
||||||
|
/// seed verisidir. Ekranin <c>InsertCommand</c>'i (ListFormSeeder_Sas) iki is yapar: eksik
|
||||||
|
/// <c>LanguageKey</c>'i metnin kimligiyle uretir ve ayni kultur + kaynak + anahtar zaten varsa metni eklemez.
|
||||||
|
/// Satir basina calistiginda 112 bin metin ~115 sn surer; burada ayni kurallar bellekte uygulanir ve iki
|
||||||
|
/// tablo toplu kopyalamayla yazilir (~2 sn).
|
||||||
|
/// </summary>
|
||||||
|
// Sinif adi arayuz adiyla bitmedigi icin ABP'nin varsayilan kaydi arayuzu acmaz; yazici listesi bos kalirdi.
|
||||||
|
[ExposeServices(typeof(IListFormSeedBulkInserter))]
|
||||||
|
public class LanguageTextSeedBulkInserter : IListFormSeedBulkInserter, ITransientDependency
|
||||||
|
{
|
||||||
|
private static readonly string KeyTable = TableNameResolver.GetFullTableName(nameof(TableNameEnum.LanguageKey));
|
||||||
|
private static readonly string TextTable = TableNameResolver.GetFullTableName(nameof(TableNameEnum.LanguageText));
|
||||||
|
|
||||||
|
private static readonly string[] KeyColumns = [nameof(LanguageKey.Id), nameof(LanguageKey.ResourceName), nameof(LanguageKey.Key)];
|
||||||
|
|
||||||
|
private static readonly string[] TextColumns =
|
||||||
|
[
|
||||||
|
nameof(LanguageText.Id), nameof(LanguageText.CultureName), nameof(LanguageText.ResourceName),
|
||||||
|
nameof(LanguageText.Key), nameof(LanguageText.Value),
|
||||||
|
];
|
||||||
|
|
||||||
|
public bool CanInsert(ListForm listForm) =>
|
||||||
|
listForm.ListFormCode == PlatformConsts.AppCodes.Languages.LanguageText;
|
||||||
|
|
||||||
|
public async Task InsertAsync(
|
||||||
|
IReadOnlyList<Dictionary<string, object>> rows,
|
||||||
|
IDynamicDataRepository repository,
|
||||||
|
string connectionString,
|
||||||
|
DataSourceTypeEnum dataSourceType)
|
||||||
|
{
|
||||||
|
// Komuttaki NOT EXISTS veritabaninin karsilastirmasini kullanir: SQL Server harf duyarsiz, PostgreSQL
|
||||||
|
// duyarlidir. Bellekteki eslesme de oyle olmali; aksi halde SQL Server'da tekil indeks, PostgreSQL'de
|
||||||
|
// yabanci anahtar ihlali olusur.
|
||||||
|
var comparer = dataSourceType == DataSourceTypeEnum.Postgresql ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
|
||||||
|
|
||||||
|
var keys = await ReadAsync(repository, connectionString, KeyTable, KeyColumns[1..], comparer);
|
||||||
|
var texts = await ReadAsync(repository, connectionString, TextTable, TextColumns[1..4], comparer);
|
||||||
|
|
||||||
|
var newKeys = new List<object[]>();
|
||||||
|
var newTexts = new List<object[]>();
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var values = TextColumns.Select(a => row.GetValueOrDefault(a)).ToArray();
|
||||||
|
|
||||||
|
if (keys.Add(Join(values[2], values[3])))
|
||||||
|
newKeys.Add([values[0], values[2], values[3]]);
|
||||||
|
|
||||||
|
if (texts.Add(Join(values[1], values[2], values[3])))
|
||||||
|
newTexts.Add(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metin anahtara yabanci anahtarla bagli; anahtarlar once yazilir.
|
||||||
|
await repository.BulkInsertAsync(KeyTable, KeyColumns, newKeys, connectionString);
|
||||||
|
await repository.BulkInsertAsync(TextTable, TextColumns, newTexts, connectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<HashSet<string>> ReadAsync(
|
||||||
|
IDynamicDataRepository repository,
|
||||||
|
string connectionString,
|
||||||
|
string table,
|
||||||
|
string[] columns,
|
||||||
|
StringComparer comparer)
|
||||||
|
{
|
||||||
|
var rows = await repository.QueryAsync(
|
||||||
|
$"SELECT {string.Join(',', columns.Select(a => $"\"{a}\""))} FROM \"{table}\"",
|
||||||
|
connectionString);
|
||||||
|
|
||||||
|
return rows
|
||||||
|
.OfType<IDictionary<string, object>>()
|
||||||
|
.Select(a => Join([.. columns.Select(c => a[c])]))
|
||||||
|
.ToHashSet(comparer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Join(params object[] values) => string.Join('\u001f', values);
|
||||||
|
}
|
||||||
|
|
@ -89,6 +89,7 @@ public class ListFormSeedDataApplier(
|
||||||
IUnitOfWorkManager unitOfWorkManager,
|
IUnitOfWorkManager unitOfWorkManager,
|
||||||
IIdentityUserRepository identityUserRepository,
|
IIdentityUserRepository identityUserRepository,
|
||||||
ILookupNormalizer lookupNormalizer,
|
ILookupNormalizer lookupNormalizer,
|
||||||
|
IEnumerable<IListFormSeedBulkInserter> bulkInserters,
|
||||||
ILogger<ListFormSeedDataApplier> logger) : IListFormSeedDataApplier, ITransientDependency
|
ILogger<ListFormSeedDataApplier> logger) : IListFormSeedDataApplier, ITransientDependency
|
||||||
{
|
{
|
||||||
/// <summary>Satir degerinde kullanicinin adiyla yazilan referans; anahtari seed sirasinda cozulur.</summary>
|
/// <summary>Satir degerinde kullanicinin adiyla yazilan referans; anahtari seed sirasinda cozulur.</summary>
|
||||||
|
|
@ -151,8 +152,8 @@ public class ListFormSeedDataApplier(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alt klasorler de taranir: satir basina dosya tutan ekranin verisi
|
// Alt klasorler de taranir: klasor modundaki ekranin verisi data/{ListFormCode}/{anahtar}.json
|
||||||
// data/{ListFormCode}/{anahtar}.json altindadir.
|
// ya da gruplu modda data/{ListFormCode}/{grup}.json altindadir.
|
||||||
var jsonFiles = Directory.GetFiles(dataPath, "*.json", SearchOption.AllDirectories);
|
var jsonFiles = Directory.GetFiles(dataPath, "*.json", SearchOption.AllDirectories);
|
||||||
if (jsonFiles.Length == 0)
|
if (jsonFiles.Length == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -274,7 +275,7 @@ public class ListFormSeedDataApplier(
|
||||||
// (bicimi degismis ekranin eski dosyasi) artik ekrana ait degildir ve satirlari kopya
|
// (bicimi degismis ekranin eski dosyasi) artik ekrana ait degildir ve satirlari kopya
|
||||||
// ya da bayat kayit uretir.
|
// ya da bayat kayit uretir.
|
||||||
if (!string.IsNullOrWhiteSpace(listForm.SeedFilePath) &&
|
if (!string.IsNullOrWhiteSpace(listForm.SeedFilePath) &&
|
||||||
!string.Equals(listForm.SeedFilePath.Replace('\\', '/').Trim('/'), target.Path, StringComparison.OrdinalIgnoreCase))
|
!string.Equals(SeedPathResolver.ParseSeedPath(listForm.SeedFilePath).Path, target.Path, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
logger.LogWarning("[{Target}] ListForm '{ListFormCode}' defines its seed target as '{SeedFilePath}', skipped.", target.Path, listFormCode, listForm.SeedFilePath);
|
logger.LogWarning("[{Target}] ListForm '{ListFormCode}' defines its seed target as '{SeedFilePath}', skipped.", target.Path, listFormCode, listForm.SeedFilePath);
|
||||||
return new ListFormSeedDataApplyResult(target.Path, 0, 0, 0, $"ListForm '{listFormCode}' seed target is '{listForm.SeedFilePath}'.");
|
return new ListFormSeedDataApplyResult(target.Path, 0, 0, 0, $"ListForm '{listFormCode}' seed target is '{listForm.SeedFilePath}'.");
|
||||||
|
|
@ -300,11 +301,15 @@ public class ListFormSeedDataApplier(
|
||||||
var existingKeys = await ReadExistingKeysAsync(listForm, keyFieldName, repository, connectionString);
|
var existingKeys = await ReadExistingKeysAsync(listForm, keyFieldName, repository, connectionString);
|
||||||
var batch = new InsertBatch();
|
var batch = new InsertBatch();
|
||||||
|
|
||||||
|
// Ekranin toplu yazicisi varsa anahtarli yeni satirlar komuta donusturulmeden biriktirilir.
|
||||||
|
var bulkInserter = bulkInserters.FirstOrDefault(a => a.CanInsert(listForm));
|
||||||
|
var bulkRows = bulkInserter == null ? null : new List<Dictionary<string, object>>();
|
||||||
|
|
||||||
foreach (var row in seedFiles.SelectMany(a => a.Rows))
|
foreach (var row in seedFiles.SelectMany(a => a.Rows))
|
||||||
{
|
{
|
||||||
var outcome = await ApplyRowAsync(
|
var outcome = await ApplyRowAsync(
|
||||||
listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType,
|
listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType,
|
||||||
overwriteExisting, existingKeys, batch);
|
overwriteExisting, existingKeys, batch, bulkRows);
|
||||||
|
|
||||||
if (batch.IsFull)
|
if (batch.IsFull)
|
||||||
await batch.FlushAsync(repository, connectionString);
|
await batch.FlushAsync(repository, connectionString);
|
||||||
|
|
@ -325,6 +330,9 @@ public class ListFormSeedDataApplier(
|
||||||
|
|
||||||
await batch.FlushAsync(repository, connectionString);
|
await batch.FlushAsync(repository, connectionString);
|
||||||
|
|
||||||
|
if (bulkRows?.Count > 0)
|
||||||
|
await bulkInserter!.InsertAsync(bulkRows, repository, connectionString, dataSourceType);
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"[{Target}] {Inserted} row(s) inserted, {Updated} row(s) updated, {Skipped} existing row(s) skipped.",
|
"[{Target}] {Inserted} row(s) inserted, {Updated} row(s) updated, {Skipped} existing row(s) skipped.",
|
||||||
target.Path, inserted, updated, skipped);
|
target.Path, inserted, updated, skipped);
|
||||||
|
|
@ -364,8 +372,9 @@ public class ListFormSeedDataApplier(
|
||||||
/// eslesir, anahtar veritabanina biraktirilir ve guncelleme yapilamaz.
|
/// eslesir, anahtar veritabanina biraktirilir ve guncelleme yapilamaz.
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Anahtarli satirin varligi <paramref name="existingKeys"/> ile bilinir ve insert'i
|
/// Anahtarli satirin varligi <paramref name="existingKeys"/> ile bilinir ve insert'i
|
||||||
/// <paramref name="batch"/>'e eklenir; anahtarsiz satir eslesmesi butun alanlara baktigi icin
|
/// <paramref name="batch"/>'e eklenir — ekranin toplu yazicisi varsa satir <paramref name="bulkRows"/>'a
|
||||||
/// eskisi gibi tek tek sorgulanir ve yazilir.
|
/// biriktirilir; anahtarsiz satir eslesmesi butun alanlara baktigi icin eskisi gibi tek tek
|
||||||
|
/// sorgulanir ve yazilir.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<SeedRowOutcome> ApplyRowAsync(
|
private async Task<SeedRowOutcome> ApplyRowAsync(
|
||||||
|
|
@ -378,7 +387,8 @@ public class ListFormSeedDataApplier(
|
||||||
DataSourceTypeEnum dataSourceType,
|
DataSourceTypeEnum dataSourceType,
|
||||||
bool overwriteExisting,
|
bool overwriteExisting,
|
||||||
HashSet<string> existingKeys,
|
HashSet<string> existingKeys,
|
||||||
InsertBatch batch)
|
InsertBatch batch,
|
||||||
|
List<Dictionary<string, object>>? bulkRows)
|
||||||
{
|
{
|
||||||
var keyField = fields.FirstOrDefault(a => a.FieldName == keyFieldName);
|
var keyField = fields.FirstOrDefault(a => a.FieldName == keyFieldName);
|
||||||
var keyValue = QueryHelper.GetFormattedValue(
|
var keyValue = QueryHelper.GetFormattedValue(
|
||||||
|
|
@ -440,6 +450,12 @@ public class ListFormSeedDataApplier(
|
||||||
insertParameters.Remove(keyFieldName);
|
insertParameters.Remove(keyFieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasKey && bulkRows != null)
|
||||||
|
{
|
||||||
|
bulkRows.Add(insertParameters);
|
||||||
|
return SeedRowOutcome.Inserted;
|
||||||
|
}
|
||||||
|
|
||||||
var insertSql = queryManager.GenerateQuery(listForm, fields, insertParameters, OperationEnum.Insert, dataSourceType, keys);
|
var insertSql = queryManager.GenerateQuery(listForm, fields, insertParameters, OperationEnum.Insert, dataSourceType, keys);
|
||||||
|
|
||||||
if (hasKey)
|
if (hasKey)
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ namespace Sozsoft.Platform.Data.Seeds;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bir ekranin veri seed hedefi. Bicim yalnizca tanimdan (<c>ListForm.SeedFilePath</c>) gelir:
|
/// Bir ekranin veri seed hedefi. Bicim yalnizca tanimdan (<c>ListForm.SeedFilePath</c>) gelir:
|
||||||
/// <c>.json</c> ile biten deger butun satirlari tasiyan tek dosyadir, uzantisiz deger her satiri
|
/// <c>.json</c> ile biten deger butun satirlari tasiyan tek dosyadir, uzantisiz deger her satiri
|
||||||
/// <c>{anahtar}.json</c> olarak tutan klasordur. Diskte ne bulundugu bu karari degistirmez; ayni
|
/// <c>{anahtar}.json</c> olarak tutan klasordur, <c>{Alan}.json</c> ile biten deger ise satirlari o
|
||||||
/// ekranin hem dosyasi hem klasoru varsa okunan, tanimin gosterdigidir.
|
/// alanin degerine gore <c>{deger}.json</c> dosyalarinda toplayan gruplu klasordur. Diskte ne bulundugu
|
||||||
|
/// bu karari degistirmez; ayni ekranin hem dosyasi hem klasoru varsa okunan, tanimin gosterdigidir.
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Hedefi okuyan ve yazan her yol (senkronizasyon, export/import, temizleme, MCP, seeder) bu sinifi
|
/// Hedefi okuyan ve yazan her yol (senkronizasyon, export/import, temizleme, MCP, seeder) bu sinifi
|
||||||
/// kullanir; dosya/klasor ayrimi ikinci bir yerde yazilmaz.
|
/// kullanir; dosya/klasor ayrimi ikinci bir yerde yazilmaz.
|
||||||
|
|
@ -21,21 +22,28 @@ public sealed class ListFormSeedTarget
|
||||||
{
|
{
|
||||||
private readonly string scopeRoot;
|
private readonly string scopeRoot;
|
||||||
|
|
||||||
private ListFormSeedTarget(string scopeRoot, string relativePath, bool isFolder)
|
private ListFormSeedTarget(string scopeRoot, string relativePath, bool isFolder, string? groupFieldName)
|
||||||
{
|
{
|
||||||
this.scopeRoot = scopeRoot;
|
this.scopeRoot = scopeRoot;
|
||||||
RelativePath = relativePath;
|
RelativePath = relativePath;
|
||||||
IsFolder = isFolder;
|
IsFolder = isFolder;
|
||||||
|
GroupFieldName = groupFieldName;
|
||||||
FullPath = Path.Combine(scopeRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
FullPath = Path.Combine(scopeRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Seed kokune gore goreli, <c>/</c> ayracli yol.</summary>
|
/// <summary>Seed kokune gore goreli, <c>/</c> ayracli yol; gruplu modda klasorun yolu.</summary>
|
||||||
public string RelativePath { get; }
|
public string RelativePath { get; }
|
||||||
|
|
||||||
public string FullPath { get; }
|
public string FullPath { get; }
|
||||||
|
|
||||||
|
/// <summary>Hedef bir klasor mu (satir basina ya da gruba gore dosya).</summary>
|
||||||
public bool IsFolder { get; }
|
public bool IsFolder { get; }
|
||||||
|
|
||||||
|
/// <summary>Gruplu klasorde dosya adini veren alan; diger bicimlerde null.</summary>
|
||||||
|
public string? GroupFieldName { get; }
|
||||||
|
|
||||||
|
public bool IsGrouped => GroupFieldName != null;
|
||||||
|
|
||||||
public bool Exists => IsFolder ? Directory.Exists(FullPath) : File.Exists(FullPath);
|
public bool Exists => IsFolder ? Directory.Exists(FullPath) : File.Exists(FullPath);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -44,7 +52,7 @@ public sealed class ListFormSeedTarget
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static ListFormSeedTarget? Create(string scopeRoot, string? seedFilePath)
|
public static ListFormSeedTarget? Create(string scopeRoot, string? seedFilePath)
|
||||||
{
|
{
|
||||||
var relativePath = seedFilePath?.Replace('\\', '/').Trim('/');
|
var (relativePath, groupFieldName) = SeedPathResolver.ParseSeedPath(seedFilePath);
|
||||||
if (string.IsNullOrWhiteSpace(relativePath) ||
|
if (string.IsNullOrWhiteSpace(relativePath) ||
|
||||||
relativePath.Contains("..", StringComparison.Ordinal) ||
|
relativePath.Contains("..", StringComparison.Ordinal) ||
|
||||||
!relativePath.StartsWith(SeedPathResolver.DataFolder + "/", StringComparison.OrdinalIgnoreCase))
|
!relativePath.StartsWith(SeedPathResolver.DataFolder + "/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
@ -54,7 +62,7 @@ public sealed class ListFormSeedTarget
|
||||||
|
|
||||||
var root = Path.GetFullPath(scopeRoot);
|
var root = Path.GetFullPath(scopeRoot);
|
||||||
|
|
||||||
return new ListFormSeedTarget(root, relativePath, SeedPathResolver.IsSeedFolderPath(relativePath));
|
return new ListFormSeedTarget(root, relativePath, SeedPathResolver.IsSeedFolderPath(seedFilePath), groupFieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Dosya yolu hedefin kendisi ya da klasor modunda onun satir dosyasi mi.</summary>
|
/// <summary>Dosya yolu hedefin kendisi ya da klasor modunda onun satir dosyasi mi.</summary>
|
||||||
|
|
@ -101,18 +109,26 @@ public sealed class ListFormSeedTarget
|
||||||
public async Task<List<Dictionary<string, object>>> ReadRowsAsync() =>
|
public async Task<List<Dictionary<string, object>>> ReadRowsAsync() =>
|
||||||
[.. (await ReadFilesAsync()).SelectMany(file => file.File.Rows ?? [])];
|
[.. (await ReadFilesAsync()).SelectMany(file => file.File.Rows ?? [])];
|
||||||
|
|
||||||
/// <summary>Klasor modunda satirin dosya yolu; anahtar dosya adina cevrilemezse null.</summary>
|
/// <summary>
|
||||||
public string? GetRowFilePath(object? key)
|
/// Klasor modunda satirin dosya yolu: satir basina modda anahtar, gruplu modda grup alaninin degeri
|
||||||
|
/// dosya adidir. Deger dosya adina cevrilemezse null.
|
||||||
|
/// </summary>
|
||||||
|
private string? GetRowFilePath(object? key, Dictionary<string, object>? row = null)
|
||||||
{
|
{
|
||||||
var fileName = SeedPathResolver.GetRowFileName(KeyText(key));
|
var name = IsGrouped ? KeyText(GroupValue(row)) : KeyText(key);
|
||||||
|
var fileName = SeedPathResolver.GetRowFileName(name);
|
||||||
|
|
||||||
return fileName == null ? null : Path.Combine(FullPath, fileName);
|
return fileName == null ? null : Path.Combine(FullPath, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object? GroupValue(Dictionary<string, object>? row) =>
|
||||||
|
row?.FirstOrDefault(a => string.Equals(a.Key, GroupFieldName, StringComparison.OrdinalIgnoreCase)).Value;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Satirlari hedefe yazar: anahtari eslesen satirin yerine gecer, yoksa eklenir. Tek dosya bir kez
|
/// Satirlari hedefe yazar: anahtari eslesen satirin yerine gecer, yoksa eklenir. Tek dosya bir kez
|
||||||
/// okunup bir kez yazilir; klasor modunda yalnizca ilgili satirin dosyasina dokunulur. Anahtari dosya
|
/// okunup bir kez yazilir; klasor modunda yalnizca ilgili satirin dosyasina, gruplu modda yalnizca
|
||||||
/// adina cevrilemeyen satirlar yazilmaz ve anahtarlari dondurulur.
|
/// satiri tasiyan dosyalara dokunulur. Dosya adi uretilemeyen (anahtari ya da grup degeri bos)
|
||||||
|
/// satirlar yazilmaz ve anahtarlari dondurulur.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<List<object>> UpsertRowsAsync(
|
public async Task<List<object>> UpsertRowsAsync(
|
||||||
string listFormCode,
|
string listFormCode,
|
||||||
|
|
@ -121,6 +137,43 @@ public sealed class ListFormSeedTarget
|
||||||
{
|
{
|
||||||
var skipped = new List<object>();
|
var skipped = new List<object>();
|
||||||
|
|
||||||
|
if (IsGrouped)
|
||||||
|
{
|
||||||
|
var items = rows.ToList();
|
||||||
|
var files = await ReadGroupFilesContainingAsync(items.Select(a => a.Key), listFormCode, keyFieldName);
|
||||||
|
|
||||||
|
foreach (var (key, row) in items)
|
||||||
|
{
|
||||||
|
var filePath = GetRowFilePath(key, row);
|
||||||
|
if (filePath == null)
|
||||||
|
{
|
||||||
|
skipped.Add(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grup alani degistiyse satir eski grubun dosyasindan cikar; kopya kalirsa migrate'te
|
||||||
|
// hangi degerin uygulanacagi dosya sirasina kalirdi.
|
||||||
|
foreach (var (path, other) in files.Loaded.Where(a => !string.Equals(a.Key, filePath, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
if (other.Rows.RemoveAll(existing => IsKey(existing, keyFieldName, key)) > 0)
|
||||||
|
files.MarkDirty(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupFile = await files.GetOrReadAsync(filePath);
|
||||||
|
var index = FindRowIndex(groupFile.Rows, keyFieldName, key);
|
||||||
|
if (index >= 0)
|
||||||
|
groupFile.Rows[index] = row;
|
||||||
|
else
|
||||||
|
groupFile.Rows.Add(row);
|
||||||
|
|
||||||
|
files.MarkDirty(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await files.SaveAsync();
|
||||||
|
|
||||||
|
return skipped;
|
||||||
|
}
|
||||||
|
|
||||||
if (IsFolder)
|
if (IsFolder)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(FullPath);
|
Directory.CreateDirectory(FullPath);
|
||||||
|
|
@ -167,12 +220,38 @@ public sealed class ListFormSeedTarget
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hedefi verilen satirlarla bastan yazar; klasor modunda listede kalmayan satirlarin dosyalari
|
/// Hedefi verilen satirlarla bastan yazar; klasor modunda listede kalmayan satirlarin dosyalari
|
||||||
/// silinir — hedef listenin aynasidir. Tek dosyada uygulama sirasi dosyanin kendi bilgisidir ve
|
/// silinir — hedef listenin aynasidir; gruplu modda satirlar grup dosyalarina dagitilir ve satiri
|
||||||
|
/// kalmayan grup dosyasi silinir. Tek dosyada uygulama sirasi dosyanin kendi bilgisidir ve
|
||||||
/// korunur; hedef henuz yoksa <paramref name="defaultOrder"/> yazilir. Anahtari dosya adina
|
/// korunur; hedef henuz yoksa <paramref name="defaultOrder"/> yazilir. Anahtari dosya adina
|
||||||
/// cevrilemeyen satirlarin sayisini dondurur.
|
/// cevrilemeyen satirlarin sayisini dondurur.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<int> ReplaceRowsAsync(string listFormCode, string keyFieldName, List<Dictionary<string, object>> rows, int defaultOrder = 0)
|
public async Task<int> ReplaceRowsAsync(string listFormCode, string keyFieldName, List<Dictionary<string, object>> rows, int defaultOrder = 0)
|
||||||
{
|
{
|
||||||
|
if (IsGrouped)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(FullPath);
|
||||||
|
|
||||||
|
var groupOrder = await ReadFolderOrderAsync() ?? defaultOrder;
|
||||||
|
var groups = rows
|
||||||
|
.Select(row => (Path: GetRowFilePath(null, row), Row: row))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var group in groups.Where(a => a.Path != null).GroupBy(a => a.Path!, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var seedFile = await ReadFileAsync(group.Key, listFormCode, keyFieldName);
|
||||||
|
seedFile.Rows = [.. group.Select(a => a.Row)];
|
||||||
|
seedFile.Order = groupOrder;
|
||||||
|
|
||||||
|
await WriteFileAsync(group.Key, seedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupPaths = groups.Where(a => a.Path != null).Select(a => a.Path!).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var filePath in GetFiles().Where(path => !groupPaths.Contains(path)))
|
||||||
|
File.Delete(filePath);
|
||||||
|
|
||||||
|
return groups.Count(a => a.Path == null);
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsFolder)
|
if (!IsFolder)
|
||||||
{
|
{
|
||||||
var isNew = !File.Exists(FullPath);
|
var isNew = !File.Exists(FullPath);
|
||||||
|
|
@ -217,6 +296,22 @@ public sealed class ListFormSeedTarget
|
||||||
/// <summary>Anahtari verilen satirlari hedeften cikarir.</summary>
|
/// <summary>Anahtari verilen satirlari hedeften cikarir.</summary>
|
||||||
public async Task RemoveRowsAsync(string listFormCode, string keyFieldName, IEnumerable<object> keys)
|
public async Task RemoveRowsAsync(string listFormCode, string keyFieldName, IEnumerable<object> keys)
|
||||||
{
|
{
|
||||||
|
if (IsGrouped)
|
||||||
|
{
|
||||||
|
var keyList = keys.ToList();
|
||||||
|
var files = await ReadGroupFilesContainingAsync(keyList, listFormCode, keyFieldName);
|
||||||
|
|
||||||
|
foreach (var (path, groupFile) in files.Loaded)
|
||||||
|
{
|
||||||
|
if (groupFile.Rows.RemoveAll(row => keyList.Any(key => IsKey(row, keyFieldName, key))) > 0)
|
||||||
|
files.MarkDirty(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
await files.SaveAsync();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (IsFolder)
|
if (IsFolder)
|
||||||
{
|
{
|
||||||
foreach (var key in keys)
|
foreach (var key in keys)
|
||||||
|
|
@ -321,13 +416,87 @@ public sealed class ListFormSeedTarget
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FindRowIndex(List<Dictionary<string, object>> rows, string keyFieldName, object key)
|
private static int FindRowIndex(List<Dictionary<string, object>> rows, string keyFieldName, object key) =>
|
||||||
{
|
rows.FindIndex(row => IsKey(row, keyFieldName, key));
|
||||||
var keyText = KeyText(key);
|
|
||||||
|
|
||||||
return rows.FindIndex(row =>
|
private static bool IsKey(Dictionary<string, object> row, string keyFieldName, object key) =>
|
||||||
row.TryGetValue(keyFieldName, out var value) &&
|
row.TryGetValue(keyFieldName, out var value) &&
|
||||||
string.Equals(KeyText(value), keyText, StringComparison.OrdinalIgnoreCase));
|
string.Equals(KeyText(value), KeyText(key), StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gruplu klasorde anahtarlardan birini tasiyan dosyalar. Dosya once metin olarak aranir, yalnizca
|
||||||
|
/// eslesen ayristirilir: dil metinlerinde her kayitta butun dillerin megabaytlarca JSON'u cozulmez.
|
||||||
|
/// Anahtar dosyada JSON kacisli yazilmis olabilecegi icin iki bicimi de aranir.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<GroupFiles> ReadGroupFilesContainingAsync(IEnumerable<object> keys, string listFormCode, string keyFieldName)
|
||||||
|
{
|
||||||
|
var files = new GroupFiles(this, listFormCode, keyFieldName);
|
||||||
|
var needles = keys
|
||||||
|
.Select(KeyText)
|
||||||
|
.Where(key => key.Length > 0)
|
||||||
|
.SelectMany(key => new[] { key, JsonSerializer.Serialize(key, ListFormSeedDataFileDto.JsonOptions)[1..^1] })
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (needles.Count == 0)
|
||||||
|
return files;
|
||||||
|
|
||||||
|
foreach (var path in GetFiles())
|
||||||
|
{
|
||||||
|
var content = await File.ReadAllTextAsync(path);
|
||||||
|
if (!needles.Any(needle => content.Contains(needle, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var file = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(content, ListFormSeedDataFileDto.JsonOptions) ?? new ListFormSeedDataFileDto();
|
||||||
|
file.ListFormCode = listFormCode;
|
||||||
|
file.KeyFieldName = keyFieldName;
|
||||||
|
file.Rows ??= [];
|
||||||
|
files.Loaded[path] = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gruplu klasorde bir islem boyunca okunan dosyalar; yalnizca degisenler yazilir, bosalan silinir.</summary>
|
||||||
|
private sealed class GroupFiles(ListFormSeedTarget target, string listFormCode, string keyFieldName)
|
||||||
|
{
|
||||||
|
private readonly HashSet<string> dirty = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public Dictionary<string, ListFormSeedDataFileDto> Loaded { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public void MarkDirty(string path) => dirty.Add(path);
|
||||||
|
|
||||||
|
public async Task<ListFormSeedDataFileDto> GetOrReadAsync(string path)
|
||||||
|
{
|
||||||
|
if (Loaded.TryGetValue(path, out var file))
|
||||||
|
return file;
|
||||||
|
|
||||||
|
var isNew = !File.Exists(path);
|
||||||
|
file = await ReadFileAsync(path, listFormCode, keyFieldName);
|
||||||
|
|
||||||
|
// Yeni grup dosyasi uygulama sirasini kardeslerinden alir; satir basina moddaki kuralin aynisi.
|
||||||
|
if (isNew)
|
||||||
|
file.Order = await target.ReadFolderOrderAsync() ?? 0;
|
||||||
|
|
||||||
|
Loaded[path] = file;
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bosalan grup dosyasi silinir: klasorde kalirsa applier onu satirsiz bir dosya olarak okurdu.</summary>
|
||||||
|
public async Task SaveAsync()
|
||||||
|
{
|
||||||
|
foreach (var path in dirty)
|
||||||
|
{
|
||||||
|
var file = Loaded[path];
|
||||||
|
if (file.Rows.Count > 0)
|
||||||
|
await WriteFileAsync(path, file);
|
||||||
|
else if (File.Exists(path))
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
dirty.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<ListFormSeedDataFileDto> ReadFileAsync(string filePath, string listFormCode, string keyFieldName)
|
private static async Task<ListFormSeedDataFileDto> ReadFileAsync(string filePath, string listFormCode, string keyFieldName)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Data.Seeds;
|
namespace Sozsoft.Platform.Data.Seeds;
|
||||||
|
|
@ -107,18 +108,47 @@ public static class SeedPathResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <c>ListForm.SeedFilePath</c> tek bir dosyayi mi yoksa satir basina dosya tutan bir klasoru mu
|
/// Bir ListForm icin satirlari bir alanin degerine gore gruplayan veri seed klasoru:
|
||||||
/// gosteriyor. Ayrim uzantidan yapilir: <c>.json</c> ile biten deger dosyadir, digerleri klasor.
|
/// <c>data/{ListFormCode}/{Alan}.json</c>. Ayni degeri tasiyan butun satirlar <c>{deger}.json</c>
|
||||||
|
/// dosyasindadir (dil metinleri: <c>{CultureName}</c> → <c>tr.json</c>). Satir basina dosya on
|
||||||
|
/// binlerce dosya uretecek, tek dosya ise her kayitta bastan yazilacak kadar buyuk ekranlar icindir.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetDataGroupedFolderPath(string listFormCode, string groupFieldName)
|
||||||
|
{
|
||||||
|
return $"{DataFolder}/{listFormCode}/{{{groupFieldName}}}.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>ListForm.SeedFilePath</c> degerini hedef yoluna ve grup alanina ayirir. Son parca
|
||||||
|
/// <c>{Alan}.json</c> ise hedef onu iceren klasordur ve satirlar o alana gore gruplanir; aksi halde
|
||||||
|
/// deger oldugu gibi hedeftir. Yol <c>/</c> ayracli ve bastan/sondan kirpilmis doner.
|
||||||
|
/// </summary>
|
||||||
|
public static (string Path, string? GroupFieldName) ParseSeedPath(string? seedFilePath)
|
||||||
|
{
|
||||||
|
var normalized = seedFilePath?.Replace('\\', '/').Trim('/') ?? string.Empty;
|
||||||
|
var match = GroupedSeedPathRegex.Match(normalized);
|
||||||
|
|
||||||
|
return match.Success
|
||||||
|
? (match.Groups["folder"].Value, match.Groups["field"].Value)
|
||||||
|
: (normalized, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>ListForm.SeedFilePath</c> tek bir dosyayi mi yoksa bir klasoru (satir basina ya da gruba gore
|
||||||
|
/// dosya) mi gosteriyor. Ayrim yoldan yapilir: <c>{Alan}.json</c> ile biten deger gruplu klasordur,
|
||||||
|
/// diger <c>.json</c> ile biten deger dosyadir, kalanlar satir basina dosya tutan klasor.
|
||||||
/// Klasor diskte henuz yoksa da karar verilebilsin diye dosya sistemine bakilmaz.
|
/// Klasor diskte henuz yoksa da karar verilebilsin diye dosya sistemine bakilmaz.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsSeedFolderPath(string? seedFilePath)
|
public static bool IsSeedFolderPath(string? seedFilePath)
|
||||||
{
|
{
|
||||||
var normalized = seedFilePath?.Replace('\\', '/').Trim('/');
|
var (path, groupFieldName) = ParseSeedPath(seedFilePath);
|
||||||
|
|
||||||
return !string.IsNullOrWhiteSpace(normalized)
|
return groupFieldName != null
|
||||||
&& !normalized.EndsWith(".json", StringComparison.OrdinalIgnoreCase);
|
|| (path.Length > 0 && !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly Regex GroupedSeedPathRegex = new(@"^(?<folder>.+)/\{(?<field>[A-Za-z_][A-Za-z0-9_]*)\}\.json$", RegexOptions.Compiled);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Klasor modunda bir satirin dosya adi: anahtar degeri. Dosya adinda kullanilamayan karakterler
|
/// Klasor modunda bir satirin dosya adi: anahtar degeri. Dosya adinda kullanilamayan karakterler
|
||||||
/// (kod tipi anahtarlarda gorulebilir) alt cizgiye cevrilir; anahtar bos ise null doner — o satir
|
/// (kod tipi anahtarlarda gorulebilir) alt cizgiye cevrilir; anahtar bos ise null doner — o satir
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,12 @@ public interface IDynamicDataRepository
|
||||||
Task<int> ExecuteAsync(string sql, string cs, Dictionary<string, object> parameters = null);
|
Task<int> ExecuteAsync(string sql, string cs, Dictionary<string, object> parameters = null);
|
||||||
Task<T> QuerySingleAsync<T>(string sql, string connectionString, Dictionary<string, object> parameters = null);
|
Task<T> QuerySingleAsync<T>(string sql, string connectionString, Dictionary<string, object> parameters = null);
|
||||||
Task<T> ExecuteScalarAsync<T>(string sql, string connectionString, Dictionary<string, object> parameters = null);
|
Task<T> ExecuteScalarAsync<T>(string sql, string connectionString, Dictionary<string, object> parameters = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Satirlari saglayicinin toplu kopyalama yoluyla (SQL Server bulk copy, PostgreSQL binary COPY) yazar;
|
||||||
|
/// on binlerce satirda satir basina komutun gidis-donus ve derleme maliyeti olmaz. Kisit ve
|
||||||
|
/// tetikleyiciler INSERT'teki gibi calisir. Her satir <paramref name="columns"/> sirasiyla degerdir.
|
||||||
|
/// </summary>
|
||||||
|
Task BulkInsertAsync(string tableName, IReadOnlyList<string> columns, IEnumerable<object[]> rows, string connectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Data.Common;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
@ -261,6 +262,28 @@ public abstract class DynamicDataRepository : IDynamicDataRepository, IUnitOfWor
|
||||||
return await dbConnection.ExecuteAsync(sql, param, transaction);
|
return await dbConnection.ExecuteAsync(sql, param, transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual async Task BulkInsertAsync(string tableName, IReadOnlyList<string> columns, IEnumerable<object[]> rows, string cs)
|
||||||
|
{
|
||||||
|
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
||||||
|
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
||||||
|
|
||||||
|
await BulkCopyAsync(
|
||||||
|
dbConnection,
|
||||||
|
transaction,
|
||||||
|
tableName,
|
||||||
|
columns,
|
||||||
|
rows.Select(row => row.Select(NormalizeParameterValue).ToArray()),
|
||||||
|
_cancellationTokenProvider.FallbackToProvider(default));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task BulkCopyAsync(
|
||||||
|
DbConnection connection,
|
||||||
|
DbTransaction transaction,
|
||||||
|
string tableName,
|
||||||
|
IReadOnlyList<string> columns,
|
||||||
|
IEnumerable<object[]> rows,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
private static DynamicParameters CreateDynamicParameters(Dictionary<string, object> parameters)
|
private static DynamicParameters CreateDynamicParameters(Dictionary<string, object> parameters)
|
||||||
{
|
{
|
||||||
var dynamicParameters = new DynamicParameters();
|
var dynamicParameters = new DynamicParameters();
|
||||||
|
|
@ -563,11 +586,76 @@ public abstract class DynamicDataRepository : IDynamicDataRepository, IUnitOfWor
|
||||||
public class MsDynamicDataRepository(
|
public class MsDynamicDataRepository(
|
||||||
IUnitOfWorkManager unitOfWorkManager,
|
IUnitOfWorkManager unitOfWorkManager,
|
||||||
ICancellationTokenProvider cancellationTokenProvider)
|
ICancellationTokenProvider cancellationTokenProvider)
|
||||||
: DynamicDataRepository(SqlClientFactory.Instance, unitOfWorkManager, cancellationTokenProvider), IScopedDependency;
|
: DynamicDataRepository(SqlClientFactory.Instance, unitOfWorkManager, cancellationTokenProvider), IScopedDependency
|
||||||
|
{
|
||||||
|
protected override async Task BulkCopyAsync(
|
||||||
|
DbConnection connection,
|
||||||
|
DbTransaction transaction,
|
||||||
|
string tableName,
|
||||||
|
IReadOnlyList<string> columns,
|
||||||
|
IEnumerable<object[]> rows,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Varsayilan bulk copy yabanci anahtari denetlemez ve kisiti "guvenilmez" isaretler; INSERT ile ayni
|
||||||
|
// davranis icin kisit ve tetikleyiciler acik.
|
||||||
|
using var bulkCopy = new SqlBulkCopy(
|
||||||
|
(SqlConnection)connection,
|
||||||
|
SqlBulkCopyOptions.CheckConstraints | SqlBulkCopyOptions.FireTriggers,
|
||||||
|
(SqlTransaction)transaction)
|
||||||
|
{
|
||||||
|
DestinationTableName = $"[{tableName}]",
|
||||||
|
BatchSize = 10000,
|
||||||
|
BulkCopyTimeout = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
using var table = new DataTable();
|
||||||
|
foreach (var column in columns)
|
||||||
|
{
|
||||||
|
table.Columns.Add(column, typeof(object));
|
||||||
|
bulkCopy.ColumnMappings.Add(column, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
table.Rows.Add(row.Select(value => value ?? DBNull.Value).ToArray());
|
||||||
|
|
||||||
|
if (table.Rows.Count > 0)
|
||||||
|
await bulkCopy.WriteToServerAsync(table, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[ExposeKeyedService<IDynamicDataRepository>("Pg")]
|
[ExposeKeyedService<IDynamicDataRepository>("Pg")]
|
||||||
public class PgDynamicDataRepository(
|
public class PgDynamicDataRepository(
|
||||||
IUnitOfWorkManager unitOfWorkManager,
|
IUnitOfWorkManager unitOfWorkManager,
|
||||||
ICancellationTokenProvider cancellationTokenProvider)
|
ICancellationTokenProvider cancellationTokenProvider)
|
||||||
: DynamicDataRepository(NpgsqlFactory.Instance, unitOfWorkManager, cancellationTokenProvider), IScopedDependency;
|
: DynamicDataRepository(NpgsqlFactory.Instance, unitOfWorkManager, cancellationTokenProvider), IScopedDependency
|
||||||
|
{
|
||||||
|
protected override async Task BulkCopyAsync(
|
||||||
|
DbConnection connection,
|
||||||
|
DbTransaction transaction,
|
||||||
|
string tableName,
|
||||||
|
IReadOnlyList<string> columns,
|
||||||
|
IEnumerable<object[]> rows,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// COPY baglantinin acik transaction'ina katilir; ayrica verilmez.
|
||||||
|
var copy = $"COPY \"{tableName}\" ({string.Join(',', columns.Select(a => $"\"{a}\""))}) FROM STDIN (FORMAT BINARY)";
|
||||||
|
|
||||||
|
await using var writer = await ((NpgsqlConnection)connection).BeginBinaryImportAsync(copy, cancellationToken);
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
await writer.StartRowAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var value in row)
|
||||||
|
{
|
||||||
|
if (value is null or DBNull)
|
||||||
|
await writer.WriteNullAsync(cancellationToken);
|
||||||
|
else
|
||||||
|
await writer.WriteAsync(value, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await writer.CompleteAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Istemci baglantiyi kapattiginda (sekme/tarayici kapanmasi, sayfa degisimi) ABP iptal token'i
|
||||||
|
/// <c>RequestAborted</c>'dir; yarida kalan sorgu <see cref="OperationCanceledException"/> atar ve
|
||||||
|
/// <c>AbpExceptionFilter</c> bunu ERR olarak loglar. Cevabi okuyacak kimse kalmadigi icin hata degildir.
|
||||||
|
/// Unit of work aksiyon filtresinde zaten geri alinmistir; burada yalnizca log ve 500 cevabi engellenir.
|
||||||
|
/// En yuksek <see cref="Order"/> sayesinde ABP'nin filtresinden once calisir.
|
||||||
|
/// </summary>
|
||||||
|
public class ClientAbortedExceptionFilter(ILogger<ClientAbortedExceptionFilter> logger) : IExceptionFilter, IOrderedFilter
|
||||||
|
{
|
||||||
|
/// <summary>nginx'in "Client Closed Request" kodu; yalnizca erisim loglarinda gorunur.</summary>
|
||||||
|
private const int ClientClosedRequest = 499;
|
||||||
|
|
||||||
|
public int Order => int.MaxValue;
|
||||||
|
|
||||||
|
public void OnException(ExceptionContext context)
|
||||||
|
{
|
||||||
|
if (context.ExceptionHandled
|
||||||
|
|| context.Exception is not OperationCanceledException
|
||||||
|
|| !context.HttpContext.RequestAborted.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Request canceled by client: {Path}", context.HttpContext.Request.Path);
|
||||||
|
|
||||||
|
context.Result = new StatusCodeResult(ClientClosedRequest);
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Extensions.DependencyInjection;
|
using Microsoft.AspNetCore.Extensions.DependencyInjection;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.ResponseCompression;
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
@ -137,6 +138,7 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
ConfigureBundles();
|
ConfigureBundles();
|
||||||
ConfigureUrls(configuration);
|
ConfigureUrls(configuration);
|
||||||
ConfigureConventionalControllers();
|
ConfigureConventionalControllers();
|
||||||
|
Configure<MvcOptions>(options => options.Filters.Add<ClientAbortedExceptionFilter>());
|
||||||
//Localization'ı veritabanından kullandıgımız icin bir json resource kullanmıyoruz
|
//Localization'ı veritabanından kullandıgımız icin bir json resource kullanmıyoruz
|
||||||
//Dolayısıyla şimdilik ihtiyacımız yok ama ileride başka bir sebepten kullanabiliriz diye silmedik
|
//Dolayısıyla şimdilik ihtiyacımız yok ama ileride başka bir sebepten kullanabiliriz diye silmedik
|
||||||
//ConfigureVirtualFileSystem(context);
|
//ConfigureVirtualFileSystem(context);
|
||||||
|
|
|
||||||
49197
configs/seeds/data/Sas.Languages.LanguageText/ar.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/ar.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/de-DE.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/de-DE.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/en.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/en.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/es.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/es.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/fi.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/fi.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/fr.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/fr.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/hi.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/hi.json
Normal file
File diff suppressed because one or more lines are too long
49197
configs/seeds/data/Sas.Languages.LanguageText/hr.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/hr.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/hu.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/hu.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/it.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/it.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/pt-BR.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/pt-BR.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/ru.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/ru.json
Normal file
File diff suppressed because one or more lines are too long
49197
configs/seeds/data/Sas.Languages.LanguageText/sk.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/sk.json
Normal file
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/sl.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/sl.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
49197
configs/seeds/data/Sas.Languages.LanguageText/zh-Hans.json
Normal file
49197
configs/seeds/data/Sas.Languages.LanguageText/zh-Hans.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -747,7 +747,7 @@ KVKK: aday verisi kişisel veridir; Adaylar ekranının yetkisi yalnızca işe a
|
||||||
| `configs/seeds/data/Hr.{CandidateSource,RejectionReason,EvaluationCriterion}.json` | Tanım verisi |
|
| `configs/seeds/data/Hr.{CandidateSource,RejectionReason,EvaluationCriterion}.json` | Tanım verisi |
|
||||||
| `configs/seeds/data/Adm.Reports.ReportTemplates.json` | KVKK formu ve teklif mektubu şablonları |
|
| `configs/seeds/data/Adm.Reports.ReportTemplates.json` | KVKK formu ve teklif mektubu şablonları |
|
||||||
| `configs/seeds/data/Adm.Notifications.Notification{Types,Rules}.json` | `IseAlimMulakatAtandi` tipi ve 2 kuralı |
|
| `configs/seeds/data/Adm.Notifications.Notification{Types,Rules}.json` | `IseAlimMulakatAtandi` tipi ve 2 kuralı |
|
||||||
| `configs/seeds/data/Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText.json` | Menü, yetki, dil satırları |
|
| `configs/seeds/data/Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText/{CultureName}.json` | Menü, yetki, dil satırları |
|
||||||
|
|
||||||
Kurulum: DB Migrate. Geri alma: Wizard Manager'dan wizard'lar silinir (alt formlar önce: `InterviewScore`
|
Kurulum: DB Migrate. Geri alma: Wizard Manager'dan wizard'lar silinir (alt formlar önce: `InterviewScore`
|
||||||
→ `Interview`/`JobOffer` → `JobApplication` → diğerleri); tablolar yalnızca onayda seçilirse düşer.
|
→ `Interview`/`JobOffer` → `JobApplication` → diğerleri); tablolar yalnızca onayda seçilirse düşer.
|
||||||
|
|
@ -1244,7 +1244,7 @@ Widget'lar liste okuma yetkisiyle görünür (İzinlerim `Hr.MyLeaveRequest`, Ek
|
||||||
| `…/Hr_S_LeaveRequestAfterSave.sql`, `Hr_S_LeaveRequestGuard.sql`, `Hr_S_LeaveNotify.sql`, `Hr_S_LeaveSickLeaveSync.sql` | Prosedürler |
|
| `…/Hr_S_LeaveRequestAfterSave.sql`, `Hr_S_LeaveRequestGuard.sql`, `Hr_S_LeaveNotify.sql`, `Hr_S_LeaveSickLeaveSync.sql` | Prosedürler |
|
||||||
| `configs/seeds/wizard/{LeaveRequest,MyLeaveRequest,TeamLeaveRequest,LeaveBalance,LeaveBalanceAdjustment,LeaveBalanceYear,MyLeaveBalance}.json` | 7 wizard; `Hr_LeaveType.json` (alan), `Hr_HealthCardSickLeave.json` (kancalar) |
|
| `configs/seeds/wizard/{LeaveRequest,MyLeaveRequest,TeamLeaveRequest,LeaveBalance,LeaveBalanceAdjustment,LeaveBalanceYear,MyLeaveBalance}.json` | 7 wizard; `Hr_LeaveType.json` (alan), `Hr_HealthCardSickLeave.json` (kancalar) |
|
||||||
| `configs/seeds/data/Sas.DeveloperKit.CustomComponents/{23744edc-eda9-512e-8079-fe2e8e5fd2a8,3b7b16c6-aa96-5765-8e19-fe5e727feb52}.json` | `MyLeaveWidget`, `TeamLeaveWidget` |
|
| `configs/seeds/data/Sas.DeveloperKit.CustomComponents/{23744edc-eda9-512e-8079-fe2e8e5fd2a8,3b7b16c6-aa96-5765-8e19-fe5e727feb52}.json` | `MyLeaveWidget`, `TeamLeaveWidget` |
|
||||||
| `configs/seeds/data/Sas.DeveloperKit.IntranetWidgets.json`, `Sas.SettingDefinitions.json`, `Adm.Notifications.Notification{Types,Rules}.json`, `Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText.json`, `Hr.LeaveType.json` | Seed satırları |
|
| `configs/seeds/data/Sas.DeveloperKit.IntranetWidgets.json`, `Sas.SettingDefinitions.json`, `Adm.Notifications.Notification{Types,Rules}.json`, `Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText/{CultureName}.json`, `Hr.LeaveType.json` | Seed satırları |
|
||||||
| `api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs` | `@Kolon` onaycı |
|
| `api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs` | `@Kolon` onaycı |
|
||||||
| `api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Adm.cs` | Kullanıcı detayı istirahat kancaları |
|
| `api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Adm.cs` | Kullanıcı detayı istirahat kancaları |
|
||||||
| `ui/src/views/admin/listForm/workflow/WorkflowCriteria.tsx` | Tasarımcıda alan onaycısı seçeneği |
|
| `ui/src/views/admin/listForm/workflow/WorkflowCriteria.tsx` | Tasarımcıda alan onaycısı seçeneği |
|
||||||
|
|
@ -1922,7 +1922,7 @@ Saat hesabı aynı günlük saat aralığının her gün tekrarlandığını var
|
||||||
| `configs/seeds/wizard/{TrainingType,TrainingSubject,Training,TrainingParticipant,TrainingFile}.json` | 5 wizard |
|
| `configs/seeds/wizard/{TrainingType,TrainingSubject,Training,TrainingParticipant,TrainingFile}.json` | 5 wizard |
|
||||||
| `configs/seeds/data/Hr.{TrainingType,TrainingSubject}.json` | Tanım verisi |
|
| `configs/seeds/data/Hr.{TrainingType,TrainingSubject}.json` | Tanım verisi |
|
||||||
| `configs/seeds/data/Adm.Reports.ReportTemplates.json` | Katılım Belgesi şablonu |
|
| `configs/seeds/data/Adm.Reports.ReportTemplates.json` | Katılım Belgesi şablonu |
|
||||||
| `configs/seeds/data/Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText.json` | Menü, yetki, dil satırları |
|
| `configs/seeds/data/Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText/{CultureName}.json` | Menü, yetki, dil satırları |
|
||||||
|
|
||||||
Kurulum: DB Migrate. Geri alma: Wizard Manager'dan alt formlar (`TrainingParticipant`, `TrainingFile`)
|
Kurulum: DB Migrate. Geri alma: Wizard Manager'dan alt formlar (`TrainingParticipant`, `TrainingFile`)
|
||||||
önce, sonra `Training`, `TrainingSubject`, `TrainingType` silinir; tablolar yalnızca onayda seçilirse
|
önce, sonra `Training`, `TrainingSubject`, `TrainingType` silinir; tablolar yalnızca onayda seçilirse
|
||||||
|
|
@ -2307,7 +2307,7 @@ yönetici (Ekip Gelişim Planı) ya da İK açar.
|
||||||
| `configs/seeds/data/Sas.DeveloperKit.CustomComponents/{b2d9fa60-b052-5039-a0fc-146f4e5b2e8c,98a5c0b9-c8d6-5e83-ae76-d9ccb5b70cba,d669129d-52fe-56e1-972d-d20fe1b3360e}.json` | `MyPerformanceWidget`, `TeamPerformanceWidget`, `My360FeedbackWidget` (code modu, rotasız) |
|
| `configs/seeds/data/Sas.DeveloperKit.CustomComponents/{b2d9fa60-b052-5039-a0fc-146f4e5b2e8c,98a5c0b9-c8d6-5e83-ae76-d9ccb5b70cba,d669129d-52fe-56e1-972d-d20fe1b3360e}.json` | `MyPerformanceWidget`, `TeamPerformanceWidget`, `My360FeedbackWidget` (code modu, rotasız) |
|
||||||
| `configs/seeds/data/Sas.DeveloperKit.IntranetWidgets.json` | `my-performance`, `team-performance`, `my-360-feedback` pano kayıtları |
|
| `configs/seeds/data/Sas.DeveloperKit.IntranetWidgets.json` | `my-performance`, `team-performance`, `my-360-feedback` pano kayıtları |
|
||||||
| `configs/seeds/data/Hr.PerformanceRating.json` | 5 kademeli derece ölçeği |
|
| `configs/seeds/data/Hr.PerformanceRating.json` | 5 kademeli derece ölçeği |
|
||||||
| `configs/seeds/data/Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText.json` | Menü, yetki, dil satırları (wizard'lara ait) |
|
| `configs/seeds/data/Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText/{CultureName}.json` | Menü, yetki, dil satırları (wizard'lara ait) |
|
||||||
| `configs/seeds/data/Adm.Notifications.Notification{Types,Rules}.json` | 2 tip, 4 kural |
|
| `configs/seeds/data/Adm.Notifications.Notification{Types,Rules}.json` | 2 tip, 4 kural |
|
||||||
| `ui/safelist.txt` | Widget bileşenlerinin Tailwind sınıfları (`twSafelistGenerator` derlemede yeniler) |
|
| `ui/safelist.txt` | Widget bileşenlerinin Tailwind sınıfları (`twSafelistGenerator` derlemede yeniler) |
|
||||||
|
|
||||||
|
|
@ -2475,7 +2475,7 @@ Bildirim, onay akışı, rapor şablonu ya da bordro bağlantısı yoktur.
|
||||||
| `configs/seeds/sql/object/Hr_T_Disciplinary{PenaltyType,Case,Defense,Decision,File}.sql` | Tablolar |
|
| `configs/seeds/sql/object/Hr_T_Disciplinary{PenaltyType,Case,Defense,Decision,File}.sql` | Tablolar |
|
||||||
| `configs/seeds/wizard/Disciplinary{PenaltyType,Case,Defense,Decision,File}.json` | 5 wizard |
|
| `configs/seeds/wizard/Disciplinary{PenaltyType,Case,Defense,Decision,File}.json` | 5 wizard |
|
||||||
| `configs/seeds/data/Hr.DisciplinaryPenaltyType.json` | Ceza türleri |
|
| `configs/seeds/data/Hr.DisciplinaryPenaltyType.json` | Ceza türleri |
|
||||||
| `configs/seeds/data/Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText.json` | Menü, yetki, dil satırları |
|
| `configs/seeds/data/Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText/{CultureName}.json` | Menü, yetki, dil satırları |
|
||||||
|
|
||||||
Kurulum: DB Migrate. Geri alma: Wizard Manager'dan alt formlar (`DisciplinaryDefense`, `DisciplinaryDecision`,
|
Kurulum: DB Migrate. Geri alma: Wizard Manager'dan alt formlar (`DisciplinaryDefense`, `DisciplinaryDecision`,
|
||||||
`DisciplinaryFile`) önce, sonra `DisciplinaryCase` ve `DisciplinaryPenaltyType` silinir; tablolar
|
`DisciplinaryFile`) önce, sonra `DisciplinaryCase` ve `DisciplinaryPenaltyType` silinir; tablolar
|
||||||
|
|
@ -2689,7 +2689,7 @@ temizlenmeden kaydedilen bir çıkış kaydı aracın km'sini 0'a çekebilir.
|
||||||
| `configs/seeds/sql/object/Hr_T_{VehicleDocument,Vehicle,VehicleFile,VehicleDestination,Driver,DriverDocument,DriverPenalty,DriverAccident,VehicleExpenseDefinition,VehicleExpense,VehicleExpenseLine,VehicleMovement}.sql` | Tablolar |
|
| `configs/seeds/sql/object/Hr_T_{VehicleDocument,Vehicle,VehicleFile,VehicleDestination,Driver,DriverDocument,DriverPenalty,DriverAccident,VehicleExpenseDefinition,VehicleExpense,VehicleExpenseLine,VehicleMovement}.sql` | Tablolar |
|
||||||
| `configs/seeds/wizard/{VehicleDocument,Vehicle,VehicleFile,VehicleDestination,Driver,DriverDocument,DriverPenalty,DriverAccident,VehicleExpenseDefinition,VehicleExpense,VehicleExpenseLine,VehicleMovement}.json` | 12 wizard |
|
| `configs/seeds/wizard/{VehicleDocument,Vehicle,VehicleFile,VehicleDestination,Driver,DriverDocument,DriverPenalty,DriverAccident,VehicleExpenseDefinition,VehicleExpense,VehicleExpenseLine,VehicleMovement}.json` | 12 wizard |
|
||||||
| `configs/seeds/data/Hr.{VehicleDocument,VehicleExpenseDefinition}.json` | Tanım verisi |
|
| `configs/seeds/data/Hr.{VehicleDocument,VehicleExpenseDefinition}.json` | Tanım verisi |
|
||||||
| `configs/seeds/data/Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText.json` | Menü, yetki, dil satırları |
|
| `configs/seeds/data/Sas.Menus.Menu.json`, `Abp.Identity.Permissions.json`, `Sas.Languages.LanguageText/{CultureName}.json` | Menü, yetki, dil satırları |
|
||||||
|
|
||||||
Kurulum: DB Migrate. Geri alma: Wizard Manager'dan alt formlar önce, ana ekranlar sonra silinir;
|
Kurulum: DB Migrate. Geri alma: Wizard Manager'dan alt formlar önce, ana ekranlar sonra silinir;
|
||||||
tablolar yalnızca onayda seçilirse düşer.
|
tablolar yalnızca onayda seçilirse düşer.
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ Uygulama ayarlarının tanım listesi (`data/Sas.SettingDefinitions.json`, `Sozs
|
||||||
| Ekran | Kod | Seed verisi |
|
| Ekran | Kod | Seed verisi |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Diller | `Sas.Languages.Language` | `data/Sas.Languages.Language.json` |
|
| Diller | `Sas.Languages.Language` | `data/Sas.Languages.Language.json` |
|
||||||
| Dil Metinleri | `Sas.Languages.LanguageText` | `data/Sas.Languages.LanguageText.json` (EN + TR, `Id` = `Sas_H_LanguageText:{ResourceName}:{Key}:{CultureName}` v5 GUID) |
|
| Dil Metinleri | `Sas.Languages.LanguageText` | `data/Sas.Languages.LanguageText/{CultureName}.json` (gruplu klasör: her dil bir dosya, ad `CultureName` — `ar, de-DE, en, es, fi, fr, hi, hr, hu, it, pt-BR, ru, sk, sl, tr, zh-Hans`; yeni anahtar hepsine eklenir; `Id` = `Sas_H_LanguageText:{ResourceName}:{Key}:{CultureName}` v5 GUID) |
|
||||||
|
|
||||||
Kullanıcıya görünen her metin dil anahtarıyla gelir (standing default 4); UI çevirisi `useLocalization`/`translateStatic`, API çevirisi `IStringLocalizer<PlatformResource>`. Tarih/sayı biçimi aktif kültürü izler (`localeFormat.ts`, `CultureFormat`; README §18.3).
|
Kullanıcıya görünen her metin dil anahtarıyla gelir (standing default 4); UI çevirisi `useLocalization`/`translateStatic`, API çevirisi `IStringLocalizer<PlatformResource>`. Tarih/sayı biçimi aktif kültürü izler (`localeFormat.ts`, `CultureFormat`; README §18.3).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -293,24 +293,21 @@ function RolesPermission({
|
||||||
|
|
||||||
if (permissionList?.groups && name) {
|
if (permissionList?.groups && name) {
|
||||||
const listPermissions = await getPermissions(providerName, name)
|
const listPermissions = await getPermissions(providerName, name)
|
||||||
const unChangedPermissions = getPermissionsWithGroupName(listPermissions.data?.groups)
|
const savedGrants = new Map(
|
||||||
const changedPermissions = getPermissionsWithGroupName(permissionList.groups)
|
getPermissionsWithGroupName(listPermissions.data?.groups).map((p) => [p.name, p.isGranted]),
|
||||||
|
|
||||||
const updatePermList = changedPermissions
|
|
||||||
.filter(
|
|
||||||
(per) =>
|
|
||||||
(unChangedPermissions.find((unChanged) => unChanged.name === per.name) || {})
|
|
||||||
.isGranted !== per.isGranted,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const updatePermList = getPermissionsWithGroupName(permissionList.groups)
|
||||||
|
.filter((per) => savedGrants.get(per.name) !== per.isGranted)
|
||||||
.map(({ name, isGranted }) => ({ name, isGranted }))
|
.map(({ name, isGranted }) => ({ name, isGranted }))
|
||||||
|
|
||||||
// 🔴 KRİTİK NOKTA
|
if (updatePermList.length) {
|
||||||
await updatePermissions(providerName, name, {
|
await updatePermissions(providerName, name, {
|
||||||
permissions: updatePermList,
|
permissions: updatePermList,
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✅ artık backend kesin güncel
|
|
||||||
await getConfig(false)
|
await getConfig(false)
|
||||||
|
}
|
||||||
|
|
||||||
notifySuccess('Permission updated')
|
notifySuccess('Permission updated')
|
||||||
}
|
}
|
||||||
|
|
@ -346,15 +343,11 @@ function RolesPermission({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPermissionsWithGroupName(groups: PermissionGroupDto[]): PermissionWithGroupName[] {
|
function getPermissionsWithGroupName(groups: PermissionGroupDto[]): PermissionWithGroupName[] {
|
||||||
return groups.reduce(
|
return groups.flatMap((group) =>
|
||||||
(acc, val) => [
|
group.permissions.map<PermissionWithGroupName>((p: any) => ({
|
||||||
...acc,
|
|
||||||
...val.permissions.map<PermissionWithGroupName>((p: any) => ({
|
|
||||||
...p,
|
...p,
|
||||||
groupName: val.name || '',
|
groupName: group.name || '',
|
||||||
})),
|
})),
|
||||||
],
|
|
||||||
[] as PermissionWithGroupName[],
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -287,24 +287,21 @@ function UsersPermission({
|
||||||
|
|
||||||
if (permissionList?.groups && id) {
|
if (permissionList?.groups && id) {
|
||||||
const listPermissions = await getPermissions(providerName, id)
|
const listPermissions = await getPermissions(providerName, id)
|
||||||
const unChangedPermissions = getPermissionsWithGroupName(listPermissions.data?.groups)
|
const savedGrants = new Map(
|
||||||
const changedPermissions = getPermissionsWithGroupName(permissionList.groups)
|
getPermissionsWithGroupName(listPermissions.data?.groups).map((p) => [p.name, p.isGranted]),
|
||||||
|
|
||||||
const updatePermList = changedPermissions
|
|
||||||
.filter(
|
|
||||||
(per) =>
|
|
||||||
(unChangedPermissions.find((unChanged) => unChanged.name === per.name) || {})
|
|
||||||
.isGranted !== per.isGranted,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const updatePermList = getPermissionsWithGroupName(permissionList.groups)
|
||||||
|
.filter((per) => savedGrants.get(per.name) !== per.isGranted)
|
||||||
.map(({ name, isGranted }) => ({ name, isGranted }))
|
.map(({ name, isGranted }) => ({ name, isGranted }))
|
||||||
|
|
||||||
// 🔴 EN KRİTİK SATIR
|
if (updatePermList.length) {
|
||||||
await updatePermissions(providerName, id, {
|
await updatePermissions(providerName, id, {
|
||||||
permissions: updatePermList,
|
permissions: updatePermList,
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✅ permission kaydı bittikten sonra config çek
|
|
||||||
await getConfig(false)
|
await getConfig(false)
|
||||||
|
}
|
||||||
|
|
||||||
notifySuccess('Permission updated')
|
notifySuccess('Permission updated')
|
||||||
}
|
}
|
||||||
|
|
@ -318,15 +315,11 @@ function UsersPermission({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPermissionsWithGroupName(groups: PermissionGroupDto[]): PermissionWithGroupName[] {
|
function getPermissionsWithGroupName(groups: PermissionGroupDto[]): PermissionWithGroupName[] {
|
||||||
return groups.reduce(
|
return groups.flatMap((group) =>
|
||||||
(acc, val) => [
|
group.permissions.map<PermissionWithGroupName>((p) => ({
|
||||||
...acc,
|
|
||||||
...val.permissions.map<PermissionWithGroupName>((p) => ({
|
|
||||||
...p,
|
...p,
|
||||||
groupName: val.name || '',
|
groupName: group.name || '',
|
||||||
})),
|
})),
|
||||||
],
|
|
||||||
[] as PermissionWithGroupName[],
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue