Language ve LanguageText ListForm Seeder taşındı

This commit is contained in:
Sedat ÖZTÜRK 2026-09-24 22:39:00 +03:00
parent 9816fb91ce
commit 246dc17fcb
17 changed files with 69515 additions and 29914 deletions

View file

@ -660,7 +660,7 @@ Platform-owned screens, menus, permissions and language texts. Changing them req
a deploy. AI must learn and teach implementation flow primarily from these assets: a deploy. AI must learn and teach implementation flow primarily from these assets:
- ListFormSeeder_Saas.cs / ListFormSeeder_Administration.cs / ListFormSeeder_{Modul}.cs - ListFormSeeder_Saas.cs / ListFormSeeder_Administration.cs / ListFormSeeder_{Modul}.cs
- LanguagesData.json - configs/seeds/host/data/App.Languages.{Language,LanguageText}.json — languages and texts (ListForm seed data)
- WizardDataSeeder.cs / CrudDataSeeder.cs — the seeders that read - WizardDataSeeder.cs / CrudDataSeeder.cs — the seeders that read
surface B below surface B below
- ListFormSeedDataApplier, CrudEndpointSeedApplier (Domain/Data/) — the single home of the apply - ListFormSeedDataApplier, CrudEndpointSeedApplier (Domain/Data/) — the single home of the apply

View file

@ -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: `App.{Modul}`, `App.{Modul}.{Liste}` - Menu/permission keys: `App.{Modul}`, `App.{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 `LanguagesData.json`. - **Localization:** Every menu, list, and field must have a corresponding entry in `configs/seeds/host/data/App.Languages.LanguageText.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,8 +30,9 @@ This document summarizes the rules, standards, and step-by-step instructions for
- **configs/seeds/host/data/App.Menus.*.json:** - **configs/seeds/host/data/App.Menus.*.json:**
- Never add, remove or re-path a route in `App.Menus.Routes.json`. The only field set you may edit on an existing row is its `Seo*` columns (lowcode §0.9). Only `App.Menus.Module.json` and `App.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 `App.Menus.Routes.json`. The only field set you may edit on an existing row is its `Seo*` columns (lowcode §0.9). Only `App.Menus.Module.json` and `App.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.
- **LanguagesData.json:** - **configs/seeds/host/data/App.Languages.LanguageText.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.
--- ---
@ -60,7 +61,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. LanguagesData.json ### 7. App.Languages.LanguageText.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`)
@ -77,7 +78,7 @@ Yeni bir modül ekle (ör: {Modul}):
- configs/seeds/host/data/Abp.Identity.PermissionGroups.json'da ayrı bir grup oluştur - configs/seeds/host/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
- LanguagesData.json'a menü, liste ve alan çevirilerini ekle - configs/seeds/host/data/App.Languages.LanguageText.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
@ -87,7 +88,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/host/data/Abp.Identity.Permissions.json'da ilgili izinleri {Modul} grubuna ekle - configs/seeds/host/data/Abp.Identity.Permissions.json'da ilgili izinleri {Modul} grubuna ekle
- LanguagesData.json'a liste ve alan çevirilerini ekle - configs/seeds/host/data/App.Languages.LanguageText.json dosyasına liste ve alan çevirilerini ekle
``` ```
### 3. Enforce Tenant Isolation and Full Audit ### 3. Enforce Tenant Isolation and Full Audit
@ -100,7 +101,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 LanguagesData.json'a şu şekilde ekle: Yeni menü, liste veya alan eklediğinde configs/seeds/host/data/App.Languages.LanguageText.json dosyasına (EN + TR satırı) şu şekilde ekle:
- App.{Modul} - App.{Modul}
- App.{Modul}.{Liste} - App.{Modul}.{Liste}
- App.Listform.ListformField.{Alan} - App.Listform.ListformField.{Alan}
@ -127,7 +128,7 @@ Yeni menü, liste veya alan eklediğinde LanguagesData.json'a şu şekilde ekle:
- [ ] 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})
- [ ] LanguagesData.json'da tüm yeni menü, liste ve alanlar var mı? - [ ] App.Languages.LanguageText.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?

View file

@ -1253,7 +1253,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** | `App.Branches` | `Sas_T_Branch` | tenant | Şube listesi. Ekranı `IsBranch = true` yaparsan filtre bu tablodan gelir; `BranchSeed` diyaloğu şube verisini kurar. | | **Branches** | `App.Branches` | `Sas_T_Branch` | tenant | Şube listesi. Ekranı `IsBranch = true` yaparsan filtre bu tablodan gelir; `BranchSeed` diyaloğu şube verisini kurar. |
| **Setting Definitions** | `App.SettingDefinitions` | `Sas_H_SettingDefinition` | host | Yeni uygulama ayarı: `Code`, `MainGroupKey`/`SubGroupKey`, `DataType`, `SelectOptions`, `DefaultValue`, `Providers`, `RequiredPermissionName`, `IsEncrypted`, `IsVisibleToClients`. Hazır tanımlar `data/App.SettingDefinitions.json`'dan gelir. Ayarlanabilir olması gereken değeri koda gömme. | | **Setting Definitions** | `App.SettingDefinitions` | `Sas_H_SettingDefinition` | host | Yeni uygulama ayarı: `Code`, `MainGroupKey`/`SubGroupKey`, `DataType`, `SelectOptions`, `DefaultValue`, `Providers`, `RequiredPermissionName`, `IsEncrypted`, `IsVisibleToClients`. Hazır tanımlar `data/App.SettingDefinitions.json`'dan gelir. Ayarlanabilir olması gereken değeri koda gömme. |
| **Languages › Language / Language Text** | `App.Languages.Language` · `.LanguageText` | `Sas_H_Language` · `Sas_H_LanguageKey` · `Sas_H_LanguageText` | host | Dil anahtarları. Üretilen her ekranın EN+TR metinleri buraya (ve `LanguagesData.json`'a) yazılır. | | **Languages › Language / Language Text** | `App.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/App.Languages.{Language,LanguageText}.json` dosyalarına senkronlanır. Metin insert'i eksik `LanguageKey`'i de üretir, aynı kültür+kaynak+anahtar varsa eklemez. |
| **Data Source** | `App.DataSource` | `Sas_H_DataSource` | host | `Code`, `DataSourceType`, `ConnectionString`. **İ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** | `App.DataSource` | `Sas_H_DataSource` | host | `Code`, `DataSourceType`, `ConnectionString`. **İ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** | `App.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/App.BackgroundWorkers.RecurringJobs.json`'dan gelir. Ayrıntı: `lowcode` §9.10, `README.md` §14. | | **Background Workers › Recurring Jobs / Jobs** | `App.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/App.BackgroundWorkers.RecurringJobs.json`'dan gelir. Ayrıntı: `lowcode` §9.10, `README.md` §14. |
| **Menus › Module / Routes / Menu / Manager** | `App.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. Seed: `data/App.Menus.{Routes,Module,Menu}.json` — `Routes` dosyasına yalnızca var olan satırın `Seo*` alanları için dokunulur. | | **Menus › Module / Routes / Menu / Manager** | `App.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. Seed: `data/App.Menus.{Routes,Module,Menu}.json` — `Routes` dosyasına yalnızca var olan satırın `Seo*` alanları için dokunulur. |
@ -1417,7 +1417,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ı + `LanguagesData.json` | | "İngilizcesi de olsun" | Saas › Languages | Dil anahtarı + `data/App.Languages.LanguageText.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` |

View file

@ -90,7 +90,7 @@ Uydurma. Şu üçünü mutlaka kontrol et:
`data/App.Menus.Menu.json` (platform menüleri), `data/Abp.Identity.PermissionGroups.json` ve `data/App.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ı** — `LanguagesData.json` ve diğer wizard dosyaları. Var olan bir 3. **Dil anahtarı çakışması** — `data/App.Languages.LanguageText.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ı
@ -470,7 +470,7 @@ ya da ziyaretçiye açık yeni bir fiziksel sayfa ürettiğinde.
| Kural | Neden | | Kural | Neden |
| --- | --- | | --- | --- |
| Metin alanları **dil anahtarı** taşır (`::App.PublicSeo.{Sayfa}.{Alan}`), anahtarlar `LanguagesData.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 (`::App.PublicSeo.{Sayfa}.{Alan}`), anahtarlar `data/App.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) |
| `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 |
@ -2417,7 +2417,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('App.…')` kullan ve dil anahtarını wizard - Kullanıcıya görünen metni gömme; `translate('App.…')` kullan ve dil anahtarını wizard
dosyasında ya da `LanguagesData.json`'da tanımla. dosyasında ya da `data/App.Languages.LanguageText.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**.
@ -3951,6 +3951,11 @@ Kurallar:
ve dosyada olmayan satır silinmez. Wizard import'unun kapanışı ise `overwriteExisting` ile ve dosyada olmayan satır silinmez. Wizard import'unun kapanışı ise `overwriteExisting` ile
çalışır: var olan satır güncellenir, soft delete izi temizlenir (§7.7). Her hedef kendi unit of çalışır: var olan satır güncellenir, soft delete izi temizlenir (§7.7). Her hedef kendi unit of
work'ündedir; birinin hatası diğerlerini durdurmaz. work'ündedir; birinin hatası diğerlerini durdurmaz.
- **Toplu yazım:** hedef tablonun anahtarları tek sorguda okunur; anahtarlı yeni satırlar ekranın
insert komutuyla (`InsertCommand` dahil) parametreleri `@p{sıra}_{ad}` olarak yeniden adlandırılıp
komut başına en fazla 2000 parametreyle toplu gönderilir. Anahtarsız satırlar tek tek eşleşir.
- **Senkronizasyon tüm listeyi yazar:** dosyanın listeyle yeniden yazıldığı durumlarda satırlar
sayfa sayfa (10000) okunur; sayfa sınırı satır kaybettirmez.
- **Yazma yönü bayrağa bağlıdır:** `list-form-data/*` ve `list-form-dynamic-api/*` uçlarında - **Yazma yönü bayrağa bağlıdır:** `list-form-data/*` ve `list-form-dynamic-api/*` uçlarında
insert → `SeedSyncInsert`, update → `SeedSyncUpdate`, delete → `SeedSyncDelete`; ImportManager insert → `SeedSyncInsert`, update → `SeedSyncUpdate`, delete → `SeedSyncDelete`; ImportManager
toplu yüklemesi `SeedSyncInsert` bayrağına bakar ve dosyayı listenin tamamıyla yeniden yazar. toplu yüklemesi `SeedSyncInsert` bayrağına bakar ve dosyayı listenin tamamıyla yeniden yazar.
@ -4068,7 +4073,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 `LanguagesData.json`'a dokunmaya gerek yoktur. Ayrıca `data/App.Languages.LanguageText.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

View file

@ -1026,7 +1026,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'`) → `LanguagesData.json` uygun klasör) → `runtimeNodes.ts` sözlüğü → `catalog.ts` tanımı (`kind: 'custom'`) → `data/App.Languages.LanguageText.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`
@ -1914,11 +1914,10 @@ Yeni ABP modülü eklemek için:
İki ayrı seed yüzeyi vardır ve karıştırılmamalıdır. İki ayrı seed yüzeyi vardır ve karıştırılmamalıdır.
**1. Depo içi (kod ile taşınan) seed'ler** — `api/src/Sozsoft.Platform.DbMigrator/Migrations/`. **1. Depo içi (kod ile taşınan) seed'ler** — `api/src/Sozsoft.Platform.DbMigrator/Migrations/`.
Platformun kendi ekranları, menüleri, yetkileri ve dil metinleri buradadır; değişiklikleri Platformun kendi ekranları buradadır (dil metinleri `configs/seeds/host/data/App.Languages.*.json`); değişiklikleri
derlenip yayınlanır. Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyalar: derlenip yayınlanır. Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyalar:
- `ListFormSeeder_Saas.cs`, `ListFormSeeder_Administration.cs` — ListForm + alan tanımları - `ListFormSeeder_Saas.cs`, `ListFormSeeder_Administration.cs` — ListForm + alan tanımları
- `LanguagesData.json` — dil anahtarları (EN/TR)
- `WizardDataSeeder.cs`, `CrudDataSeeder.cs` — aşağıdaki - `WizardDataSeeder.cs`, `CrudDataSeeder.cs` — aşağıdaki
runtime seed dosyalarını okuyup uygulayan seeder'lar runtime seed dosyalarını okuyup uygulayan seeder'lar
- `ListFormSeedDataApplier`, `CrudEndpointSeedApplier` (`Domain/Data/`) — uygulama kuralının tek - `ListFormSeedDataApplier`, `CrudEndpointSeedApplier` (`Domain/Data/`) — uygulama kuralının tek
@ -1940,6 +1939,7 @@ configs/seeds/
│ │ # App.Definitions.{CountryGroup,Country,City,District}.json — adres zinciri │ │ # App.Definitions.{CountryGroup,Country,City,District}.json — adres zinciri
│ │ # App.Definitions.{Currency,ContactTitle,AiBot}.json — para birimi, hitap, AI bot │ │ # App.Definitions.{Currency,ContactTitle,AiBot}.json — para birimi, hitap, AI bot
│ │ # App.Notifications.Notification{Types,Rules}.json — bildirim tipi → kuralı │ │ # App.Notifications.Notification{Types,Rules}.json — bildirim tipi → kuralı
│ │ # App.Languages.{Language,LanguageText}.json — dil → metin (eksik LanguageKey metinle üretilir)
│ │ # App.SettingDefinitions.json, App.BackgroundWorkers.RecurringJobs.json — ayar tanımları, zamanlanmış işler │ │ # App.SettingDefinitions.json, App.BackgroundWorkers.RecurringJobs.json — ayar tanımları, zamanlanmış işler
│ ├── sql/{object,execute}/ # .sql (SQL Server) │ ├── sql/{object,execute}/ # .sql (SQL Server)
│ └── postgres/{object,execute}/ │ └── postgres/{object,execute}/

View file

@ -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 (LanguagesData.json) okunur; modul ikinci bir dil /// <c>PlatformResource</c> kaynagindan (configs/seeds/host/data/App.Languages.LanguageText.json) okunur; modul ikinci bir dil
/// kaynagi acmaz. /// kaynagi acmaz.
/// </summary> /// </summary>
[DependsOn( [DependsOn(

View file

@ -1,7 +1,7 @@
namespace Sozsoft.Mcp; namespace Sozsoft.Mcp;
/// <summary> /// <summary>
/// MCP hatalarinin dil anahtarlari. Karsiliklari platformun <c>LanguagesData.json</c> /// MCP hatalarinin dil anahtarlari. Karsiliklari platformun <c>configs/seeds/host/data/App.Languages.LanguageText.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

View file

@ -211,16 +211,7 @@ public class ListFormSeedDataSynchronizer(
/// <summary>Hedefi listenin tamamiyla yeniden yazar.</summary> /// <summary>Hedefi listenin tamamiyla yeniden yazar.</summary>
private async Task WriteAllRowsAsync(ListForm listForm, SeedTarget target) private async Task WriteAllRowsAsync(ListForm listForm, SeedTarget target)
{ {
var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto var rows = await ReadAllRowsAsync(listForm);
{
ListFormCode = listForm.ListFormCode,
Skip = 0,
Take = SyncAllPageSize,
RequireTotalCount = false,
RequireGroupCount = false,
});
List<Dictionary<string, object>> rows = ToRows((object)result?.Data);
if (target.IsFolder) if (target.IsFolder)
{ {
@ -235,13 +226,36 @@ public class ListFormSeedDataSynchronizer(
await WriteSeedFileAsync(target.Path, seedFile); await WriteSeedFileAsync(target.Path, seedFile);
} }
/// <summary>
/// Listenin butun satirlarini sayfa sayfa okur. Tek dosya her yazimda listeyle degistirildigi
/// icin tek sayfayla yetinilirse sayfa sinirini asan satirlar dosyadan sessizce duser.
/// </summary>
private async Task<List<Dictionary<string, object>>> ReadAllRowsAsync(ListForm listForm)
{
var rows = new List<Dictionary<string, object>>();
while (true)
{
var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto
{
ListFormCode = listForm.ListFormCode,
Skip = rows.Count,
Take = SyncAllPageSize,
RequireTotalCount = false,
RequireGroupCount = false,
});
List<Dictionary<string, object>> page = ToRows((object)result?.Data);
rows.AddRange(page);
if (page.Count < SyncAllPageSize)
return rows;
}
}
/// <summary> /// <summary>
/// Klasor modunda her satiri kendi dosyasina yazar ve listede kalmayan satirlarin dosyalarini /// Klasor modunda her satiri kendi dosyasina yazar ve listede kalmayan satirlarin dosyalarini
/// siler — klasor listenin aynasidir. /// siler — klasor listenin aynasidir.
/// <para>
/// Temizlik yalnizca liste tek sayfaya sigdiginda yapilir: sayfa sinirina dayanan bir listede
/// gorulmeyen satirlarin dosyasi silinirse veri kaybolur.
/// </para>
/// </summary> /// </summary>
private async Task WriteRowFilesAsync(ListForm listForm, SeedTarget target, List<Dictionary<string, object>> rows) private async Task WriteRowFilesAsync(ListForm listForm, SeedTarget target, List<Dictionary<string, object>> rows)
{ {
@ -266,9 +280,6 @@ public class ListFormSeedDataSynchronizer(
written.Add(fileName); written.Add(fileName);
} }
if (rows.Count >= SyncAllPageSize)
return;
foreach (var filePath in Directory.EnumerateFiles(target.Path, "*.json")) foreach (var filePath in Directory.EnumerateFiles(target.Path, "*.json"))
{ {
if (!written.Contains(Path.GetFileName(filePath))) if (!written.Contains(Path.GetFileName(filePath)))

View file

@ -1,112 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Sozsoft.Languages.Entities;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.Data.Seeds;
public class LanguageTextsSeedDto
{
public string ResourceName { get; set; }
public string Key { get; set; }
public string En { get; set; }
public string Tr { get; set; }
}
public class LanguageSeederDto
{
public List<Language> Languages { get; set; }
public List<LanguageTextsSeedDto> LanguageTexts { get; set; }
}
public class LanguageDataSeeder : IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Language, Guid> _languages;
private readonly IRepository<LanguageKey, Guid> _languageKey;
private readonly IRepository<LanguageText, Guid> _languagesText;
public LanguageDataSeeder(
IRepository<Language, Guid> languages,
IRepository<LanguageKey, Guid> languageKey,
IRepository<LanguageText, Guid> languagesText)
{
_languages = languages;
_languageKey = languageKey;
_languagesText = languagesText;
}
public async Task SeedAsync(DataSeedContext context)
{
var languages = await _languages.GetListAsync();
var keys = await _languageKey.GetListAsync();
var texts = await _languagesText.GetListAsync();
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(Path.Combine("Migrations", "LanguagesData.json"))
.Build();
var items = configuration.Get<LanguageSeederDto>();
foreach (var item in items.Languages)
{
if (!languages.Any(a => a.CultureName == item.CultureName))
{
await _languages.InsertAsync(new()
{
CultureName = item.CultureName,
UiCultureName = item.UiCultureName,
DisplayName = item.DisplayName,
IsEnabled = item.IsEnabled,
TwoLetterISOLanguageName = new CultureInfo(item.CultureName).TwoLetterISOLanguageName,
});
}
}
foreach (var item in items.LanguageTexts)
{
try
{
if (!keys.Any(a => a.Key == item.Key))
{
await _languageKey.InsertAsync(new()
{
Key = item.Key,
ResourceName = item.ResourceName,
});
}
if (!texts.Any(a => a.CultureName == "en" && a.Key == item.Key))
{
await _languagesText.InsertAsync(new()
{
CultureName = "en",
Key = item.Key,
Value = item.En,
ResourceName = item.ResourceName,
});
}
if (!texts.Any(a => a.CultureName == "tr" && a.Key == item.Key))
{
await _languagesText.InsertAsync(new()
{
CultureName = "tr",
Key = item.Key,
Value = item.Tr,
ResourceName = item.ResourceName,
});
}
}
catch (Exception ex)
{
throw new Exception($"Hata veren Kod:' ResourceName='{item.ResourceName}', Key='{item.Key}' Message='{ex.Message}'");
}
}
}
}

View file

@ -4017,6 +4017,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Language)), SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Language)),
KeyFieldName = "Id", KeyFieldName = "Id",
KeyFieldDbSourceType = DbType.Guid, KeyFieldDbSourceType = DbType.Guid,
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
SeedSyncInsert = true,
SeedSyncUpdate = true,
SeedSyncDelete = true,
SortMode = GridOptions.SortModeSingle, SortMode = GridOptions.SortModeSingle,
FilterRowJson = DefaultFilterRowJson(), FilterRowJson = DefaultFilterRowJson(),
HeaderFilterJson = DefaultHeaderFilterJson(), HeaderFilterJson = DefaultHeaderFilterJson(),
@ -4026,7 +4030,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
ColumnOptionJson = DefaultColumnOptionJson(), ColumnOptionJson = DefaultColumnOptionJson(),
PermissionJson = DefaultPermissionJson(listFormName), PermissionJson = DefaultPermissionJson(listFormName),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), // Tabloda audit kolonu yoktur; varsayilan insert degerleri yalnizca anahtari uretir.
InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
}),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 450, true, true, true, true, false, true), EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 450, true, true, true, true, false, true),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() { EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() { new() {
@ -4140,14 +4147,35 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
ListFormCode = listForm.ListFormCode, ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
SourceDbType = DbType.Boolean, SourceDbType = DbType.Boolean,
FieldName = "IsEnabled", FieldName = "TwoLetterISOLanguageName",
EditGroupOrderNo = 1, EditGroupOrderNo = 1,
EditOrderNo = 4, EditOrderNo = 4,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 1,
CaptionName = "App.Listform.ListformField.TwoLetterISOLanguageName",
Width = 0,
ListOrderNo = 5,
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Boolean,
FieldName = "IsEnabled",
EditGroupOrderNo = 1,
EditOrderNo = 5,
EditorType2 = EditorTypes.dxCheckBox, EditorType2 = EditorTypes.dxCheckBox,
ColSpan = 1, ColSpan = 1,
CaptionName = "App.Listform.ListformField.IsEnabled", CaptionName = "App.Listform.ListformField.IsEnabled",
Width = 0, Width = 0,
ListOrderNo = 5, ListOrderNo = 6,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -4186,6 +4214,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.LanguageText)), SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.LanguageText)),
KeyFieldName = "Id", KeyFieldName = "Id",
KeyFieldDbSourceType = DbType.Guid, KeyFieldDbSourceType = DbType.Guid,
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
SeedSyncInsert = true,
SeedSyncUpdate = true,
SeedSyncDelete = true,
SortMode = GridOptions.SortModeSingle, SortMode = GridOptions.SortModeSingle,
FilterRowJson = DefaultFilterRowJson(), FilterRowJson = DefaultFilterRowJson(),
HeaderFilterJson = DefaultHeaderFilterJson(), HeaderFilterJson = DefaultHeaderFilterJson(),
@ -4198,7 +4230,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 400, true, true, true, true, false), EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 400, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() { EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() { Order=1, ColCount=1, ColSpan=1, ItemType="group" },}), new() { Order=1, ColCount=1, ColSpan=1, ItemType="group" },}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
}),
InsertCommand = LanguageTextInsertCommand(),
CommandColumnJson = JsonSerializer.Serialize(new CommandColumnDto[] { CommandColumnJson = JsonSerializer.Serialize(new CommandColumnDto[] {
new() { new() {
ButtonPosition= UiCommandButtonPositionTypeEnum.Toolbar, ButtonPosition= UiCommandButtonPositionTypeEnum.Toolbar,
@ -12311,4 +12346,25 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
#endregion #endregion
} }
/// <summary>
/// Metin, (ResourceName, Key) yabanci anahtariyla <c>LanguageKey</c>'e baglidir; anahtar tablosunun
/// ekrani ve seed dosyasi yoktur, bu yuzden eksik anahtar metinle birlikte uretilir. Anahtarin
/// kimligi metnin <c>@Id</c>'sidir: iki saglayicida da calisan tek GUID kaynagi odur.
/// 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.
/// Son select gercek kaydin anahtarini dondurur; senkronizasyon satiri onunla okur.
/// </summary>
private static string LanguageTextInsertCommand()
{
var keyTable = TableNameResolver.GetFullTableName(nameof(TableNameEnum.LanguageKey));
var textTable = TableNameResolver.GetFullTableName(nameof(TableNameEnum.LanguageText));
const string textMatch = "\"CultureName\" = @CultureName AND \"ResourceName\" = @ResourceName AND \"Key\" = @Key";
return $"INSERT INTO \"{keyTable}\" (\"Id\",\"ResourceName\",\"Key\") SELECT @Id, @ResourceName, @Key " +
$"WHERE NOT EXISTS (SELECT 1 FROM \"{keyTable}\" WHERE \"ResourceName\" = @ResourceName AND \"Key\" = @Key); " +
$"INSERT INTO \"{textTable}\" (\"Id\",\"CultureName\",\"ResourceName\",\"Key\",\"Value\") SELECT @Id, @CultureName, @ResourceName, @Key, @Value " +
$"WHERE NOT EXISTS (SELECT 1 FROM \"{textTable}\" WHERE {textMatch}); " +
$"SELECT \"Id\" FROM \"{textTable}\" WHERE {textMatch}";
}
} }

View file

@ -29,11 +29,6 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<None Remove="Migrations\LanguagesData.json" />
<Content Include="Migrations\LanguagesData.json">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,8 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -67,7 +70,8 @@ public enum SeedRowOutcome
/// Ekranlardan girilen veri <c>ListForm.SeedFilePath</c> ile bir seed dosyasina aynalanir; /// Ekranlardan girilen veri <c>ListForm.SeedFilePath</c> ile bir seed dosyasina aynalanir;
/// bu servis ayni dosyayi ters yonde uygular. Satirlar anahtar alan uzerinden eslesir: kayit /// bu servis ayni dosyayi ters yonde uygular. Satirlar anahtar alan uzerinden eslesir: kayit
/// yoksa eklenir, varsa dokunulmaz. Boylece her migrate calistiginda veritabanindaki guncel /// yoksa eklenir, varsa dokunulmaz. Boylece her migrate calistiginda veritabanindaki guncel
/// kayitlar dosyadaki eski degerlerle ezilmez ve kopya kayit olusmaz. /// kayitlar dosyadaki eski degerlerle ezilmez ve kopya kayit olusmaz. Eklenecek satirlar toplu
/// komutlarla yazilir.
/// <para> /// <para>
/// Sorgu uretimi <see cref="IQueryManager.GenerateQuery"/> ile yapilir; ikinci bir SQL uretici /// Sorgu uretimi <see cref="IQueryManager.GenerateQuery"/> ile yapilir; ikinci bir SQL uretici
/// yazilmaz. Yetki suzgecinden gecen <c>ListFormFieldManager</c> yerine alanlar dogrudan /// yazilmaz. Yetki suzgecinden gecen <c>ListFormFieldManager</c> yerine alanlar dogrudan
@ -318,9 +322,22 @@ public class ListFormSeedDataApplier(
var updated = 0; var updated = 0;
var skipped = 0; var skipped = 0;
// Anahtarli satirlarin varligi satir basina sorulmaz: tablonun anahtarlari bir kez okunur,
// yeni satirlar toplu komutlarla yazilir. Binlerce satirlik bir dosyada (dil metinleri)
// satir basina iki gidis-donus seed suresini dakikalara cikarir.
var existingKeys = await ReadExistingKeysAsync(listForm, keyFieldName, repository, connectionString);
var batch = new InsertBatch();
foreach (var row in seedFiles.SelectMany(a => a.Rows)) foreach (var row in seedFiles.SelectMany(a => a.Rows))
{ {
switch (await ApplyRowAsync(listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType, overwriteExisting)) var outcome = await ApplyRowAsync(
listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType,
overwriteExisting, existingKeys, batch);
if (batch.ParameterCount >= InsertBatch.MaxParameterCount)
await batch.FlushAsync(repository, connectionString);
switch (outcome)
{ {
case SeedRowOutcome.Inserted: case SeedRowOutcome.Inserted:
inserted++; inserted++;
@ -334,6 +351,8 @@ public class ListFormSeedDataApplier(
} }
} }
await batch.FlushAsync(repository, connectionString);
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);
@ -371,6 +390,11 @@ public class ListFormSeedDataApplier(
/// true ise dosyadaki degerlerle guncellenir ve soft delete izleri temizlenir, degilse dokunulmaz. /// true ise dosyadaki degerlerle guncellenir ve soft delete izleri temizlenir, degilse dokunulmaz.
/// Dosyada anahtar yoksa (identity kolonlu tablolar) satir diger alanlarinin tamami uzerinden /// Dosyada anahtar yoksa (identity kolonlu tablolar) satir diger alanlarinin tamami uzerinden
/// eslesir, anahtar veritabanina biraktirilir ve guncelleme yapilamaz. /// eslesir, anahtar veritabanina biraktirilir ve guncelleme yapilamaz.
/// <para>
/// Anahtarli satirin varligi <paramref name="existingKeys"/> ile bilinir ve insert'i
/// <paramref name="batch"/>'e eklenir; anahtarsiz satir eslesmesi butun alanlara baktigi icin
/// eskisi gibi tek tek sorgulanir ve yazilir.
/// </para>
/// </summary> /// </summary>
private async Task<SeedRowOutcome> ApplyRowAsync( private async Task<SeedRowOutcome> ApplyRowAsync(
ListForm listForm, ListForm listForm,
@ -380,7 +404,9 @@ public class ListFormSeedDataApplier(
IDynamicDataRepository repository, IDynamicDataRepository repository,
string connectionString, string connectionString,
DataSourceTypeEnum dataSourceType, DataSourceTypeEnum dataSourceType,
bool overwriteExisting) bool overwriteExisting,
HashSet<string> existingKeys,
InsertBatch batch)
{ {
var keyField = fields.FirstOrDefault(a => a.FieldName == keyFieldName); var keyField = fields.FirstOrDefault(a => a.FieldName == keyFieldName);
var keyValue = QueryHelper.GetFormattedValue( var keyValue = QueryHelper.GetFormattedValue(
@ -393,16 +419,12 @@ public class ListFormSeedDataApplier(
var resolvedRow = await ResolveTokensAsync(row); var resolvedRow = await ResolveTokensAsync(row);
var insertParameters = BuildParameters(fields, resolvedRow, keyFieldName, includeKey: hasKey); var insertParameters = BuildParameters(fields, resolvedRow, keyFieldName, includeKey: hasKey);
var match = hasKey // Ayni anahtar dosyada iki kez gecerse ikincisi de "var" sayilir; toplu komutta kopya olusmaz.
? new Dictionary<string, object> { [keyFieldName] = keyValue } var exists = hasKey
: new Dictionary<string, object>(insertParameters); ? !existingKeys.Add(ToKeyText(keyValue))
: await ExistsAsync(listForm, BuildKeylessMatch(listForm, insertParameters), repository, connectionString);
if (!hasKey && PlatformConsts.IsMultiTenant && listForm.IsTenant) if (exists)
{
match["TenantId"] = currentTenant.Id;
}
if (await ExistsAsync(listForm, match, repository, connectionString))
{ {
// Anahtarsiz satirin hangi kayda karsilik geldigi bilinmedigi icin guncellenemez. // Anahtarsiz satirin hangi kayda karsilik geldigi bilinmedigi icin guncellenemez.
if (!overwriteExisting || !hasKey) if (!overwriteExisting || !hasKey)
@ -447,10 +469,84 @@ public class ListFormSeedDataApplier(
} }
var insertSql = queryManager.GenerateQuery(listForm, fields, insertParameters, OperationEnum.Insert, dataSourceType, keys); var insertSql = queryManager.GenerateQuery(listForm, fields, insertParameters, OperationEnum.Insert, dataSourceType, keys);
await repository.ExecuteAsync(insertSql, connectionString, insertParameters);
if (hasKey)
batch.Add(insertSql, insertParameters);
else
await repository.ExecuteAsync(insertSql, connectionString, insertParameters);
return SeedRowOutcome.Inserted; return SeedRowOutcome.Inserted;
} }
/// <summary>
/// Hedef tablodaki anahtar degerleri. Anahtar karsilastirmasi metin uzerindendir
/// (<see cref="ToKeyText"/>); dosyadaki deger ile veritabanindan donen deger farkli tipte gelir.
/// Soft delete edilmis satirlar da doner: kayit tabloda durdugu surece yeniden eklenemez.
/// </summary>
private static async Task<HashSet<string>> ReadExistingKeysAsync(
ListForm listForm,
string keyFieldName,
IDynamicDataRepository repository,
string connectionString)
{
var rows = await repository.QueryAsync(
$"SELECT \"{keyFieldName}\" FROM \"{listForm.SelectCommand}\"",
connectionString);
return rows
.OfType<IDictionary<string, object>>()
.Select(a => a.Values.FirstOrDefault())
.Where(a => a != null && a is not DBNull)
.Select(ToKeyText)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
}
private static string ToKeyText(object value) =>
Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
/// <summary>
/// Insert komutlarini tek bir komut metninde biriktirir. Her satirin parametreleri
/// <c>@p{sira}_{ad}</c> olarak yeniden adlandirilir; boylece ekranin kendi insert komutu
/// (<c>InsertCommand</c>) da degismeden toplu calisir. Komut basina parametre sayisi SQL Server
/// sinirinin (2100) altinda tutulur.
/// </summary>
private sealed class InsertBatch
{
public const int MaxParameterCount = 2000;
private static readonly Regex ParameterRegex = new(@"(?<![@\w])@(\w+)", RegexOptions.Compiled);
private readonly StringBuilder sql = new();
private readonly Dictionary<string, object> parameters = [];
private int rowCount;
public int ParameterCount => parameters.Count;
public void Add(string rowSql, Dictionary<string, object> rowParameters)
{
var prefix = $"p{rowCount++}_";
sql.Append(ParameterRegex.Replace(rowSql, m =>
rowParameters.ContainsKey(m.Groups[1].Value) ? $"@{prefix}{m.Groups[1].Value}" : m.Value));
sql.Append(";\n");
foreach (var item in rowParameters)
parameters[prefix + item.Key] = item.Value;
}
public async Task FlushAsync(IDynamicDataRepository repository, string connectionString)
{
if (rowCount == 0)
return;
await repository.ExecuteAsync(sql.ToString(), connectionString, parameters);
sql.Clear();
parameters.Clear();
rowCount = 0;
}
}
/// <summary> /// <summary>
/// Ekranin silme komutunun yazdigi soft delete kolonlarini bulup onlari sifirlayan /// Ekranin silme komutunun yazdigi soft delete kolonlarini bulup onlari sifirlayan
/// parametreleri dondurur. Kolon listesi komuttan okunur; boylece yalnizca gercekten var olan /// parametreleri dondurur. Kolon listesi komuttan okunur; boylece yalnizca gercekten var olan
@ -476,6 +572,17 @@ public class ListFormSeedDataApplier(
return parameters; return parameters;
} }
/// <summary>Anahtarsiz satirin eslesme kosulu: butun alan degerleri, tenant ekranda kapsam da.</summary>
private Dictionary<string, object> BuildKeylessMatch(ListForm listForm, Dictionary<string, object> parameters)
{
var match = new Dictionary<string, object>(parameters);
if (PlatformConsts.IsMultiTenant && listForm.IsTenant)
match["TenantId"] = currentTenant.Id;
return match;
}
/// <summary>Verilen alan degerleriyle eslesen bir kayit hedef tabloda var mi.</summary> /// <summary>Verilen alan degerleriyle eslesen bir kayit hedef tabloda var mi.</summary>
private static async Task<bool> ExistsAsync( private static async Task<bool> ExistsAsync(
ListForm listForm, ListForm listForm,

View file

@ -191,7 +191,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` + `LanguagesData.json`. Yetki ekranındaki sıra `AbpPermissions.Order` `permission.constant.ts` + `data/App.Languages.LanguageText.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.
@ -288,7 +288,8 @@ 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ı** → `LanguagesData.json`; eklemeden önce anahtarın zaten var olup - **Yeni bir dil anahtarı** → `configs/seeds/host/data/App.Languages.LanguageText.json` (EN + TR iki satır;
`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. 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`
@ -359,7 +360,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
şehir → ilçe (`App.Definitions.{CountryGroup,Country,City,District}.json`), bildirim tipi → kuralı şehir → ilçe (`App.Definitions.{CountryGroup,Country,City,District}.json`), bildirim tipi → kuralı
(`App.Notifications.Notification{Types,Rules}.json`). Para birimi, hitap, AI bot (`App.Notifications.Notification{Types,Rules}.json`). Para birimi, hitap, AI bot
(`App.Definitions.{Currency,ContactTitle,AiBot}.json`), ayar tanımları (`App.SettingDefinitions.json`) (`App.Definitions.{Currency,ContactTitle,AiBot}.json`), ayar tanımları (`App.SettingDefinitions.json`)
ve zamanlanmış işler (`App.BackgroundWorkers.RecurringJobs.json`) da buradadır; `HostData.json` ve zamanlanmış işler (`App.BackgroundWorkers.RecurringJobs.json`), dil → metin
(`App.Languages.{Language,LanguageText}.json`; `LanguagesData.json` + `LanguageDataSeeder` kaldırıldı) da buradadır; `HostData.json`
kaldırıldı, `HostDataSeeder` yalnızca bağlantı cümlesi appsettings'ten gelen varsayılan veri kaynağını kurar. Dosyalar ekranın kaldırıldı, `HostDataSeeder` yalnızca bağlantı cümlesi appsettings'ten gelen varsayılan veri kaynağını kurar. Dosyalar ekranın
anahtarını (`KeyFieldName`) kullanır; `Guid` anahtarlı satırın `Id`'si iş anahtarından türetilmiş anahtarını (`KeyFieldName`) kullanır; `Guid` anahtarlı satırın `Id`'si iş anahtarından türetilmiş
sabit bir GUID'dir, satır eklerken aynı kuralla üret. `App.Menus.Routes.json` dosyasına sabit bir GUID'dir, satır eklerken aynı kuralla üret. `App.Menus.Routes.json` dosyasına

View file

@ -0,0 +1,136 @@
{
"ListFormCode": "App.Languages.Language",
"KeyFieldName": "Id",
"GeneratedAt": "2026-09-24T19:17:56.868760Z",
"Order": 1,
"Rows": [
{
"Id": "d9541ac8-390c-5611-8f34-40ee6d3c8c54",
"CultureName": "ar",
"UiCultureName": "ar",
"DisplayName": "العربية",
"IsEnabled": true,
"TwoLetterISOLanguageName": "ar"
},
{
"Id": "7eb292ce-e8f0-5b41-805a-863dfdbf46f3",
"CultureName": "de-DE",
"UiCultureName": "de-DE",
"DisplayName": "Deutsch",
"IsEnabled": true,
"TwoLetterISOLanguageName": "de"
},
{
"Id": "72e8ca2c-a01b-5857-a7b6-1ce03e26968d",
"CultureName": "en",
"UiCultureName": "en",
"DisplayName": "English",
"IsEnabled": true,
"TwoLetterISOLanguageName": "en"
},
{
"Id": "827c6542-089b-5df1-bf7b-bac686530c6a",
"CultureName": "es",
"UiCultureName": "es",
"DisplayName": "Español",
"IsEnabled": true,
"TwoLetterISOLanguageName": "es"
},
{
"Id": "8adf7238-64a0-5141-b9ff-fd21c18f5942",
"CultureName": "fi",
"UiCultureName": "fi",
"DisplayName": "Finnish",
"IsEnabled": true,
"TwoLetterISOLanguageName": "fi"
},
{
"Id": "728c95bb-119c-55b0-b807-f3eb744fba46",
"CultureName": "fr",
"UiCultureName": "fr",
"DisplayName": "Français",
"IsEnabled": true,
"TwoLetterISOLanguageName": "fr"
},
{
"Id": "6915db67-5a62-5dba-8304-8c51edcb2c6e",
"CultureName": "hi",
"UiCultureName": "hi",
"DisplayName": "Hindi",
"IsEnabled": true,
"TwoLetterISOLanguageName": "hi"
},
{
"Id": "81aa40ad-2dd7-58c9-8fcf-81abd92a05b5",
"CultureName": "hr",
"UiCultureName": "hr",
"DisplayName": "Croatian",
"IsEnabled": true,
"TwoLetterISOLanguageName": "hr"
},
{
"Id": "5622a2a2-98e3-56b3-b13c-df260d3f6bf9",
"CultureName": "hu",
"UiCultureName": "hu",
"DisplayName": "Magyar",
"IsEnabled": true,
"TwoLetterISOLanguageName": "hu"
},
{
"Id": "be35f5d2-5eb9-5ed6-b1bb-4da462a92070",
"CultureName": "it",
"UiCultureName": "it",
"DisplayName": "Italiano",
"IsEnabled": true,
"TwoLetterISOLanguageName": "it"
},
{
"Id": "2fe16fd9-23ec-520f-971c-3ff529aecb24",
"CultureName": "pt-BR",
"UiCultureName": "pt-BR",
"DisplayName": "Português",
"IsEnabled": true,
"TwoLetterISOLanguageName": "pt"
},
{
"Id": "d42bd1b1-5aa7-5f7e-a0c9-af13abd971d4",
"CultureName": "ru",
"UiCultureName": "ru",
"DisplayName": "Русский",
"IsEnabled": true,
"TwoLetterISOLanguageName": "ru"
},
{
"Id": "0694a5a5-e444-5e55-a36c-0e170c34749e",
"CultureName": "sk",
"UiCultureName": "sk",
"DisplayName": "Slovak",
"IsEnabled": true,
"TwoLetterISOLanguageName": "sk"
},
{
"Id": "a94d08f3-467a-596d-87b1-a5177ba5d4e8",
"CultureName": "sl",
"UiCultureName": "sl",
"DisplayName": "Slovenščina",
"IsEnabled": true,
"TwoLetterISOLanguageName": "sl"
},
{
"Id": "a768d2e0-b77d-5ed1-a8b1-93ffa253db36",
"CultureName": "tr",
"UiCultureName": "tr",
"DisplayName": "Türkçe",
"IsEnabled": true,
"TwoLetterISOLanguageName": "tr"
},
{
"Id": "30dbe4b4-4137-5ab1-ad9f-2d8403232cf7",
"CultureName": "zh-Hans",
"UiCultureName": "zh-Hans",
"DisplayName": "繁體中文",
"IsEnabled": true,
"TwoLetterISOLanguageName": "zh"
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -256,7 +256,7 @@
"SelectOptions": "{}", "SelectOptions": "{}",
"IsVisibleToClients": false, "IsVisibleToClients": false,
"IsInherited": false, "IsInherited": false,
"IsEncrypted": true "IsEncrypted": false
}, },
{ {
"Id": "81a25360-a113-5dc3-bc42-8629286abdf8", "Id": "81a25360-a113-5dc3-bc42-8629286abdf8",