diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 0b5fd367..2d9fc436 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -53,7 +53,8 @@ configs/seeds/host/ ├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise) ├── crud/{EntityName}.json ← tablonun REST uçları ├── custom/{ComponentName}.json ← Custom Component (Visual Designer dokümanı ya da kod) -└── wizard/{WizardName}.json ← ekran + menü + yetki + dil anahtarları +├── wizard/{WizardName}.json ← ekran + menü + yetki + dil anahtarları +└── data/{ListFormCode}.json ← ekrandan girilen liste verisinin aynası (opsiyonel) ``` Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen aynıdır. @@ -67,6 +68,7 @@ Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen ayn | 3 | Custom Component (gerekiyorsa) | `custom/{Name}.json` | Wizard `Custom` yolunda bileşen adını arar | | 4 | Wizard | `wizard/{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir | | 5 | Alt ekranlar (varsa) | ek `wizard/…` dosyaları | Ana wizard'dan **önceki** `SeededAt` damgasını alır | +| 6 | Liste verisi (isteniyorsa) | `data/{ListFormCode}.json` | Ekran ve tablo hazır olmalı; `ListForm.SeedFilePath` bu yolu gösterir | **`Wizard.SeededAt` bir bağımlılık aracıdır.** Dosya adı tarih taşımaz (`{WizardName}.json`); `WizardDataSeeder` `wizard/` klasörünü **`Wizard.SeededAt` damgasına göre sıralı** işler (eşitlikte @@ -3402,6 +3404,44 @@ kullanılacağı** `lowcode-reference.instructions.md` §12'dedir. --- +## 9.1. Artefakt G — Liste verisi seed dosyası (`data/{ListFormCode}.json`) + +Ekrandan girilen kayıtların (widget tanımı, parametre tablosu, sabit liste) veritabanı +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. Ekrandaki hangi işlemin dosyaya +yansıyacağını `SeedSyncInsert`, `SeedSyncUpdate` ve `SeedSyncDelete` bayrakları belirler — üçü de +kapalıyken dosya yalnızca seed sırasında okunur, hiç yazılmaz. + +```jsonc +{ + "ListFormCode": "App.DeveloperKit.IntranetWidgets", + "KeyFieldName": "Id", + "GeneratedAt": "2026-09-04T00:00:00Z", + "Rows": [ + { "Id": "542e0ab0-…", "Code": "documents", "Column": 0, "Order": 20, "IsActive": true } + ] +} +``` + +| Alan | Sözleşme | +| --- | --- | +| `ListFormCode` | Verinin ait olduğu ekran; dosya adıyla aynı olmalı. Boşsa dosya adı kullanılır | +| `KeyFieldName` | Satırların eşleştiği anahtar alan; boşsa `ListForm.KeyFieldName` | +| `Rows` | Ekranın select alanlarından oluşan satırlar. Enum'lar **sayı** olarak yazılır (SQL kolonu ne ise o); dil anahtarları `::` öneki olmadan yazılır | + +Kurallar: + +- **Anahtar dosyada durur.** Insert varsayılanları anahtarı `@NEWID` ile üretir; `ListFormSeedDataApplier` + dosyadaki anahtarı geri yazar, böylece aynı dosya birden çok kez uygulandığında kopya kayıt oluşmaz. + Elle dosya yazarken anahtarı deterministik üret (ör. koddan türeyen GUID). +- **Uygulama yalnızca ekler:** anahtarı veritabanında bulunmayan satırlar `INSERT` edilir; var olan + kayıt güncellenmez (her migrate'te ekrandaki güncel değerler dosyadaki eskisiyle ezilmesin diye) + ve dosyada olmayan satır silinmez. +- **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 + toplu yüklemesi `SeedSyncInsert` bayrağına bakar ve dosyayı listenin tamamıyla yeniden yazar. +- Audit/tenant kolonları dosyaya yazılmaz; `InsertFieldsDefaultValueJson` + kapsam üretir. + ## 10. Uçtan uca reçeteler ### 10.1 "Var olan bir tablo için ekran aç" diff --git a/.gitignore b/.gitignore index c2cafb96..2785d9b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +# Konteyner veri klasorleri (mssql/postgres/redis vb.); seed veri dosyalari haric tutulur. configs/**/data/** +!configs/seeds/**/data/** *.bak **/node_modules **/.DS_Store diff --git a/README.md b/README.md index ab79cc0a..ae587df8 100644 --- a/README.md +++ b/README.md @@ -245,17 +245,17 @@ runtime'da düzenlenebilen seed'ler `configs/seeds/` altındadır — bkz. `App: `configs/seeds` altında en üst seviye kapsamdır (CDN klasörleriyle aynı mantık): host veritabanı için `host/`, her tenant için `tenants/{tenantId}/`. Kapsamın altında içerik -türüne göre `crud/`, `custom/`, `wizard/`, `sql/`, `postgres/` klasörleri bulunur: +türüne göre `crud/`, `custom/`, `wizard/`, `data/`, `sql/`, `postgres/` klasörleri bulunur: ``` configs/seeds/ host/ - crud/ custom/ wizard/ + crud/ custom/ wizard/ data/ sql/ { object/ execute/ } postgres/ { object/ execute/ } tenants/ {tenantId}/ - crud/ custom/ wizard/ + crud/ custom/ wizard/ data/ sql/ { object/ execute/ } postgres/ { object/ execute/ } ``` @@ -270,6 +270,7 @@ Aktif veritabanı sağlayıcısına göre `sql/` (SQL Server) ya da `postgres/` | `wizard` | ListForm Wizard'ın ürettiği `.json` konfigürasyonları; `WizardDataSeeder` kendi kapsamındaki dosyaları okur. | | `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` kendi kapsamındaki dosyaları okur. | | `custom` | Custom Component konfigürasyonları (`{Name}.json`); Component Manager kaydetme/silme sırasında dosyayı senkronlar, `CustomComponentDataSeeder` kendi kapsamındaki dosyaları okuyup veritabanına uygular. | +| `data` | Ekranlardan girilen listelerin veri aynası (`{ListFormCode}.json`). `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. | 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. @@ -372,6 +373,8 @@ Ekran davranışı büyük ölçüde JSON kolonlarında saklanır. Öne çıkanl | `ColumnOptionJson`, `PagerOptionJson`, `SelectionJson`, `StateStoringJson` | Sütun, sayfalama, seçim ve durum saklama ayarları. | | `Insert/Update/DeleteCommand` (+ `Before` / `After`) | CRUD komutları ve öncesi/sonrası çalışacak SQL kancaları. | | `Insert/Update/DeleteServiceAddress` | Varsayılan olarak `list-form-data/*`; özel endpoint'e yönlendirilebilir. | +| `SeedFilePath` | Doluysa seed sırasında listenin verisi kapsam klasöründeki bu JSON dosyasından geri yüklenir (`data/{ListFormCode}.json`). | +| `SeedSyncInsert` / `SeedSyncUpdate` / `SeedSyncDelete` | Ekranda yapılan hangi işlemin seed dosyasına yansıyacağı; kapalıysa o işlem dosyaya dokunmaz. | | `SubFormsJson` | Ana-detay ilişkisi (ParentFieldName → ChildFieldName eşlemesi). | | `WidgetsJson` | Ekranın üstünde gösterilecek KPI kartları. | | `WorkflowJson` | İş akışı bağlaması. | @@ -798,7 +801,7 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer | Aktivite ve denetim kaydı | `/admin/activityLog`, `/admin/changeLog` | Kullanıcı hareketleri ve ABP audit log detayı. | | Bildirimler | `/admin/profile/notification-settings`, bildirim ekranları | Kanal tercihleri ve bildirim oluşturma. | | Forum | `/admin/forum`, `/admin/forumManagement` | Forum kullanımı ve yönetimi. | -| Intranet | Intranet dashboard ve widget'ları | Duyuru, anket, sosyal duvar, etkinlik. Hangi widget'ın hangi kolonda, hangi sırada ve hangi yetkiyle yayınlandığı `Adm_T_IntranetWidget` kaydından gelir (`TenantData.json` → `Widgets`). | +| Intranet | Intranet dashboard ve widget'ları | Duyuru, anket, sosyal duvar, etkinlik. Hangi widget'ın hangi kolonda, hangi sırada ve hangi yetkiyle yayınlandığı `Adm_T_IntranetWidget` kaydından gelir; kayıtlar `configs/seeds/{kapsam}/data/App.DeveloperKit.IntranetWidgets.json` ile senkron tutulur. | | Video oda | `/admin/videoroom/*` | Oda listesi, detay, planlama, katılımcı ve ekran paylaşımı. | | Raporlar | `/admin/reports/:report/view|design/...` | DevExpress rapor görüntüleyici ve tasarımcı. | | AI asistanı | `/admin/ai` | n8n webhook üzerinden sohbet arayüzü. | diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridOptionsEditDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridOptionsEditDto.cs index 3a79599d..f7863fbd 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridOptionsEditDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/GridOptionsEditDto.cs @@ -32,6 +32,19 @@ public class GridOptionsEditDto : GridOptionsDto set { SelectFieldsDefaultValueJson = JsonSerializer.Serialize(value); } } + /// Kapsam klasorune gore goreli seed dosyasi yolu (ornegin data/{ListFormCode}.json). + /// Dolu ise ekrandan yapilan kayit islemleri dosyaya yansitilir. + public string SeedFilePath { get; set; } + + /// Ekrandan eklenen kayit seed dosyasina da eklenir. + public bool SeedSyncInsert { get; set; } + + /// Ekrandan guncellenen kaydin seed dosyasindaki karsiligi guncellenir. + public bool SeedSyncUpdate { get; set; } + + /// Ekrandan silinen kaydin satiri seed dosyasindan cikarilir. + public bool SeedSyncDelete { get; set; } + public string DeleteCommand { get; set; } public string DeleteBeforeCommand { get; set; } public string DeleteAfterCommand { get; set; } @@ -170,6 +183,10 @@ public class GridOptionsEditDto : GridOptionsDto target.DeleteCommand = DeleteCommand; target.DeleteBeforeCommand = DeleteBeforeCommand; target.DeleteAfterCommand = DeleteAfterCommand; + target.SeedFilePath = SeedFilePath; + target.SeedSyncInsert = SeedSyncInsert; + target.SeedSyncUpdate = SeedSyncUpdate; + target.SeedSyncDelete = SeedSyncDelete; target.SelectFieldsDefaultValueJson = SelectFieldsDefaultValueJson; target.InsertFieldsDefaultValueJson = InsertFieldsDefaultValueJson; diff --git a/api/src/Sozsoft.Platform.Application/ListForms/Administration/ListFormsAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/Administration/ListFormsAppService.cs index b3b7d189..11ddbe5a 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/Administration/ListFormsAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/Administration/ListFormsAppService.cs @@ -89,6 +89,10 @@ public class ListFormsAppService : CrudAppService< item.IsTenant = input.IsTenant; item.IsBranch = input.IsBranch; item.IsOrganizationUnit = input.IsOrganizationUnit; + item.SeedFilePath = input.SeedFilePath; + item.SeedSyncInsert = input.SeedSyncInsert; + item.SeedSyncUpdate = input.SeedSyncUpdate; + item.SeedSyncDelete = input.SeedSyncDelete; } else if (input.EditType == ListFormEditTabs.Database.Select.SelectForm) { diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs index f4b585de..93bf3067 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs @@ -17,17 +17,20 @@ public class ListFormDataAppService : PlatformAppService private readonly IQueryManager qManager; private readonly IHttpContextAccessor httpContextAccessor; private readonly IListFormSelectAppService listFormSelectAppService; + private readonly IListFormSeedDataSynchronizer seedDataSynchronizer; public ListFormDataAppService( IListFormAuthorizationManager authManager, IQueryManager qManager, IHttpContextAccessor httpContextAccessor, - IListFormSelectAppService listFormSelectAppService) + IListFormSelectAppService listFormSelectAppService, + IListFormSeedDataSynchronizer seedDataSynchronizer) { this.authManager = authManager; this.qManager = qManager; this.httpContextAccessor = httpContextAccessor; this.listFormSelectAppService = listFormSelectAppService; + this.seedDataSynchronizer = seedDataSynchronizer; LocalizationResource = typeof(PlatformResource); } @@ -41,7 +44,11 @@ public class ListFormDataAppService : PlatformAppService ?? throw new InvalidOperationException("HTTP Context bulunamadı."); var queryParameters = httpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value); - return await qManager.GenerateAndRunQueryAsync(input.ListFormCode, OperationEnum.Insert, input.Data, queryParameters: queryParameters); + var insertedKey = await qManager.GenerateAndRunQueryAsync(input.ListFormCode, OperationEnum.Insert, input.Data, queryParameters: queryParameters); + + await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, (object)insertedKey, SeedSyncOperation.Insert); + + return insertedKey; } public async Task PostDuplicateAsync(DataRequestDto input) @@ -74,7 +81,11 @@ public class ListFormDataAppService : PlatformAppService input.Data = JsonSerializer.Serialize(dict); - return await qManager.GenerateAndRunQueryAsync(input.ListFormCode, OperationEnum.Duplicate, input.Data, null, queryParameters); + var duplicatedKey = await qManager.GenerateAndRunQueryAsync(input.ListFormCode, OperationEnum.Duplicate, input.Data, null, queryParameters); + + await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, (object)duplicatedKey, SeedSyncOperation.Insert); + + return duplicatedKey; } public async Task PostUpdateAsync(DataRequestDto input) @@ -87,7 +98,11 @@ public class ListFormDataAppService : PlatformAppService ?? throw new InvalidOperationException("HTTP Context bulunamadı."); var queryParameters = httpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value); - return await qManager.GenerateAndRunQueryAsync(input.ListFormCode, OperationEnum.Update, input.Data, input.Keys, queryParameters); + var affected = await qManager.GenerateAndRunQueryAsync(input.ListFormCode, OperationEnum.Update, input.Data, input.Keys, queryParameters); + + await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, input.Keys?.FirstOrDefault(), SeedSyncOperation.Update); + + return affected; } public async Task PostDeleteAsync(DataRequestDto input) @@ -100,7 +115,11 @@ public class ListFormDataAppService : PlatformAppService ?? throw new InvalidOperationException("HTTP Context bulunamadı."); var queryParameters = httpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value); - return await qManager.GenerateAndRunQueryAsync(input.ListFormCode, OperationEnum.Delete, input.Data, input.Keys, queryParameters); + var affected = await qManager.GenerateAndRunQueryAsync(input.ListFormCode, OperationEnum.Delete, input.Data, input.Keys, queryParameters); + + await seedDataSynchronizer.RemoveRowsAsync(input.ListFormCode, input.Keys); + + return affected; } } diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs index 05b79e7f..e900655f 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs @@ -35,6 +35,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic private readonly IConfiguration configuration; private readonly IOptions identityOptions; private readonly IRepository orderRepository; + private readonly IListFormSeedDataSynchronizer seedDataSynchronizer; public ListFormDynamicApiAppService( ITenantRepository tenantRepository, @@ -44,7 +45,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic BlobManager blobCdnManager, IConfiguration configuration, IOptions identityOptions, - IRepository orderRepository) + IRepository orderRepository, + IListFormSeedDataSynchronizer seedDataSynchronizer) { this.tenantRepository = tenantRepository; this.tenantManager = tenantManager; @@ -54,6 +56,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic this.configuration = configuration; this.identityOptions = identityOptions; this.orderRepository = orderRepository; + this.seedDataSynchronizer = seedDataSynchronizer; } private static Guid ParseGuid(string value) @@ -126,6 +129,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic (await userManager.CreateAsync(user, input.Data.Password)).CheckErrors(); await userManager.SetLockoutEnabledAsync(user, true); + + await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, user.Id, SeedSyncOperation.Insert); } [Authorize(IdentityPermissions.Users.Update)] @@ -198,18 +203,22 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic await SaveAvatarAsync(user, input.Data.Avatar); (await userManager.UpdateAsync(user)).CheckErrors(); + + await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, user.Id, SeedSyncOperation.Update); } //RoleAppService [Authorize(IdentityPermissions.Roles.Create)] public async Task PostRoleInsertAsync(DynamicApiBaseInput input) { - await identityRoleAppService.CreateAsync(new IdentityRoleCreateDto + var role = await identityRoleAppService.CreateAsync(new IdentityRoleCreateDto { Name = input.Data.Name, IsDefault = input.Data.IsDefault ?? false, IsPublic = input.Data.IsPublic ?? false, }); + + await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, role.Id, SeedSyncOperation.Insert); } [Authorize(IdentityPermissions.Roles.Update)] @@ -229,12 +238,16 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic }; await identityRoleAppService.UpdateAsync(id, role); + + await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, id, SeedSyncOperation.Update); } [Authorize(TenantManagementPermissions.Tenants.Create)] public async Task PostTenantInsertAsync(DynamicApiBaseInput input) { - await CreateTenantAsync(input.Data); + var tenant = await CreateTenantAsync(input.Data); + + await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, tenant.Id, SeedSyncOperation.Insert); } [Authorize(TenantManagementPermissions.Tenants.Create)] @@ -353,6 +366,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic entity.SetModules(input.Data.Modules); await tenantRepository.UpdateAsync(entity, autoSave: true); + + await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, id, SeedSyncOperation.Update); } [Authorize(TenantManagementPermissions.Tenants.Delete)] @@ -364,6 +379,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic } var id = Guid.Parse(input.Keys[0]!.ToString()!); await tenantRepository.DeleteAsync(id); + + await seedDataSynchronizer.RemoveRowsAsync(input.ListFormCode, input.Keys); } } diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormImportAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormImportAppService.cs index a93aa86f..96efd697 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormImportAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormImportAppService.cs @@ -28,13 +28,15 @@ public class ListFormImportAppService : PlatformAppService, IImportAppService private readonly IListFormAuthorizationManager _authManager; private readonly IQueryManager _qManager; private readonly BlobManager _blobContainer; + private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer; public ListFormImportAppService( IRepository importSessionRepository, IRepository importSessionExecuteRepository, IListFormAuthorizationManager authManager, IQueryManager qManager, - BlobManager blobContainer + BlobManager blobContainer, + IListFormSeedDataSynchronizer seedDataSynchronizer ) { _importSessionRepository = importSessionRepository; @@ -42,6 +44,7 @@ public class ListFormImportAppService : PlatformAppService, IImportAppService _blobContainer = blobContainer; _authManager = authManager; _qManager = qManager; + _seedDataSynchronizer = seedDataSynchronizer; } public async Task CreateAsync( @@ -224,6 +227,10 @@ public class ListFormImportAppService : PlatformAppService, IImportAppService session.Status = errorCount > 0 ? "executed_with_errors" : "executed"; await _importSessionRepository.UpdateAsync(session, autoSave: true); + // Toplu yuklemede satir bazli senkronizasyon anlamsiz; dosya listenin tamamiyla yazilir. + if (validCount > 0) + await _seedDataSynchronizer.SyncAllAsync(request.ListFormCode); + return ObjectMapper.Map(execute); } catch (Exception ex) diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSeedDataSynchronizer.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSeedDataSynchronizer.cs new file mode 100644 index 00000000..32c48493 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSeedDataSynchronizer.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Sozsoft.Platform.Data.Seeds; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.ListForms.Select; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; + +namespace Sozsoft.Platform.ListForms; + +/// +/// Ekrandan yapilan kayit islemlerini, ListForm'un bagli oldugu veri seed dosyasina yansitir. +/// +public interface IListFormSeedDataSynchronizer +{ + /// + /// Eklenen ya da guncellenen satiri veritabanindan okuyup dosyadaki karsiligina yazar. + /// Ekranin ilgili senkronizasyon bayragi kapaliysa hicbir sey yapmaz. Anahtar bilinmiyorsa + /// ya da satir okunamiyorsa dosya listenin tamamiyla yenilenir. + /// + Task SyncRowAsync(string listFormCode, object key, SeedSyncOperation operation); + + /// Silinen satirlari dosyadan cikarir; SeedSyncDelete kapaliysa hicbir sey yapmaz. + Task RemoveRowsAsync(string listFormCode, object[] keys); + + /// + /// Dosyayi listenin tamamiyla yeniden yazar; toplu islemler (import) icin kullanilir. + /// Toplu yukleme kayit ekledigi icin SeedSyncInsert bayragina baglidir. + /// + Task SyncAllAsync(string listFormCode); +} + +/// Senkronizasyonu tetikleyen islem; ekranin hangi bayraginin bakilacagini belirler. +public enum SeedSyncOperation +{ + Insert, + Update +} + +/// +/// ListForm.SeedFilePath dolu olan ekranlarin verisi kapsam klasorundeki bir JSON dosyasinda +/// aynalanir; veritabani sifirlandiginda ListFormSeedDataApplier ayni dosyadan geri yukler. +/// Senkronizasyon satir bazlidir: yalnizca islem goren satir okunup dosyadaki karsiligina yazilir, +/// boylece her kayitta listenin tamami sorgulanmaz ve dosyadaki satir sirasi korunur. +/// +public class ListFormSeedDataSynchronizer( + IRepository listFormRepository, + IListFormSelectAppService listFormSelectAppService, + IConfiguration configuration, + ICurrentTenant currentTenant, + ILogger logger) : IListFormSeedDataSynchronizer, ITransientDependency +{ + private const int SyncAllPageSize = 10000; + + public async Task SyncRowAsync(string listFormCode, object key, SeedSyncOperation operation) + { + await ExecuteAsync(listFormCode, IsEnabled(operation), async (listForm, filePath) => + { + // Insert ucu her veri kaynaginda anahtari geri dondurmez; anahtarsiz kaldigimizda + // ya da satir okunamadiginda dosya listenin tamamiyla yenilenir, bayat kalmaz. + var row = key == null ? null : await ReadRowAsync(listForm, key); + if (row == null) + { + logger.LogInformation("Row '{Key}' could not be read for '{ListFormCode}', seed file is rewritten from the list.", key, listForm.ListFormCode); + await WriteAllRowsAsync(listForm, filePath); + return; + } + + var seedFile = await ReadSeedFileAsync(filePath, listForm); + var index = FindRowIndex(seedFile, listForm.KeyFieldName, key); + + if (index >= 0) + seedFile.Rows[index] = row; + else + seedFile.Rows.Add(row); + + await WriteSeedFileAsync(filePath, seedFile); + }); + } + + public async Task RemoveRowsAsync(string listFormCode, object[] keys) + { + await ExecuteAsync(listFormCode, listForm => listForm.SeedSyncDelete, async (listForm, filePath) => + { + if (keys == null || keys.Length == 0 || !File.Exists(filePath)) + { + await WriteAllRowsAsync(listForm, filePath); + return; + } + + var seedFile = await ReadSeedFileAsync(filePath, listForm); + var removed = false; + + foreach (var key in keys) + { + var index = FindRowIndex(seedFile, listForm.KeyFieldName, key); + if (index < 0) + continue; + + seedFile.Rows.RemoveAt(index); + removed = true; + } + + if (removed) + await WriteSeedFileAsync(filePath, seedFile); + }); + } + + public async Task SyncAllAsync(string listFormCode) + { + await ExecuteAsync(listFormCode, listForm => listForm.SeedSyncInsert, WriteAllRowsAsync); + } + + private static Func IsEnabled(SeedSyncOperation operation) + { + return operation == SeedSyncOperation.Insert + ? listForm => listForm.SeedSyncInsert + : listForm => listForm.SeedSyncUpdate; + } + + /// Dosyayi listenin tamamiyla yeniden yazar. + private async Task WriteAllRowsAsync(ListForm listForm, string filePath) + { + var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto + { + ListFormCode = listForm.ListFormCode, + Skip = 0, + Take = SyncAllPageSize, + RequireTotalCount = false, + RequireGroupCount = false, + }); + + await WriteSeedFileAsync(filePath, new ListFormSeedDataFileDto + { + ListFormCode = listForm.ListFormCode, + KeyFieldName = listForm.KeyFieldName, + Rows = ToRows((object)result?.Data) + }); + } + + /// + /// Seed dosyasi bagli degilse ya da islemin bayragi kapaliysa hicbir sey yapmaz. + /// Senkronizasyon hatasi kullanicinin kayit islemini bozmaz; yalnizca loglanir. + /// + private async Task ExecuteAsync( + string listFormCode, + Func isEnabled, + Func action) + { + if (string.IsNullOrWhiteSpace(listFormCode)) + return; + + try + { + var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode); + if (listForm == null || string.IsNullOrWhiteSpace(listForm.SeedFilePath) || !isEnabled(listForm)) + return; + + var filePath = SeedPathResolver.GetScopePath( + configuration, + currentTenant.Id, + listForm.SeedFilePath.Replace('\\', '/').Split('/')); + + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); + + await action(listForm, filePath); + } + catch (Exception ex) + { + logger.LogError(ex, "Seed data file could not be synchronized for '{ListFormCode}': {Message}", listFormCode, ex.Message); + } + } + + private async Task> ReadRowAsync(ListForm listForm, object key) + { + object[] filter = [listForm.KeyFieldName, "=", key]; + + var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto + { + ListFormCode = listForm.ListFormCode, + Filter = JsonSerializer.Serialize(filter), + Skip = 0, + Take = 1, + RequireTotalCount = false, + RequireGroupCount = false, + }); + + // Data dinamik oldugu icin once statik tipe alinir; aksi halde LINQ eklentileri + // calisma zamaninda cozulemez. + List> rows = ToRows((object)result?.Data); + + return rows.FirstOrDefault(); + } + + /// Select sonucundaki dinamik satirlari dosyaya yazilabilir sozluklere cevirir. + private static List> ToRows(object data) + { + var rows = new List>(); + + if (data is not IEnumerable items) + return rows; + + foreach (var item in items) + { + if (item is IDictionary dictionary) + rows.Add(new Dictionary(dictionary)); + } + + return rows; + } + + private static int FindRowIndex(ListFormSeedDataFileDto seedFile, string keyFieldName, object key) + { + var keyText = ToComparableText(key); + + return seedFile.Rows.FindIndex(row => + row.TryGetValue(keyFieldName, out var value) && + string.Equals(ToComparableText(value), keyText, StringComparison.OrdinalIgnoreCase)); + } + + /// Anahtarlar dosyada JsonElement, istekte string/Guid olarak gelir; metin uzerinden karsilastirilir. + private static string ToComparableText(object value) + { + return value switch + { + null => string.Empty, + JsonElement { ValueKind: JsonValueKind.String } element => element.GetString() ?? string.Empty, + JsonElement element => element.ToString(), + _ => value.ToString() ?? string.Empty + }; + } + + private static async Task ReadSeedFileAsync(string filePath, ListForm listForm) + { + var seedFile = File.Exists(filePath) + ? JsonSerializer.Deserialize( + await File.ReadAllTextAsync(filePath), + ListFormSeedDataFileDto.JsonOptions) + : null; + + seedFile ??= new ListFormSeedDataFileDto(); + seedFile.ListFormCode = listForm.ListFormCode; + seedFile.KeyFieldName = listForm.KeyFieldName; + seedFile.Rows ??= []; + + return seedFile; + } + + private static async Task WriteSeedFileAsync(string filePath, ListFormSeedDataFileDto seedFile) + { + seedFile.GeneratedAt = DateTime.UtcNow; + await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(seedFile, ListFormSeedDataFileDto.JsonOptions)); + } +} diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index c174d6b1..40b248d7 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -4602,6 +4602,12 @@ "en": "Control Type", "tr": "Kontrol Türü" }, + { + "resourceName": "Platform", + "key": "App.ListFormEdit.ScopeOptions", + "en": "Scope", + "tr": "Kapsam" + }, { "resourceName": "Platform", "key": "App.ListFormEdit.IsTenant", @@ -4752,6 +4758,42 @@ "en": "Assignments to be made to the fields when Save is pressed while updating the record", "tr": "Kayıt güncellenirken Kaydet'e basılınca alanlara yapılacak atamalar" }, + { + "resourceName": "Platform", + "key": "App.ListFormEdit.SeedSyncOperations", + "en": "Seed File Synchronization", + "tr": "Seed Dosyası Senkronizasyonu" + }, + { + "resourceName": "Platform", + "key": "App.ListFormEdit.SeedSyncInsert", + "en": "Inserts", + "tr": "Eklemeler" + }, + { + "resourceName": "Platform", + "key": "App.ListFormEdit.SeedSyncUpdate", + "en": "Updates", + "tr": "Güncellemeler" + }, + { + "resourceName": "Platform", + "key": "App.ListFormEdit.SeedSyncDelete", + "en": "Deletes", + "tr": "Silmeler" + }, + { + "resourceName": "Platform", + "key": "App.ListFormEdit.SeedFilePath", + "en": "Seed File Path", + "tr": "Seed Dosya Yolu" + }, + { + "resourceName": "Platform", + "key": "App.ListFormEdit.SeedFilePathHint", + "en": "Relative to the scope folder, e.g. data/{ListFormCode}.json. When filled, insert/update/delete operations are mirrored to this file.", + "tr": "Kapsam klasörüne göre görelidir, örneğin data/{ListFormCode}.json. Dolu ise ekleme/güncelleme/silme işlemleri bu dosyaya yansıtılır." + }, { "resourceName": "Platform", "key": "App.ListFormEdit.DatabaseDeleteServiceAddress", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs index 0a767794..65a4c4f6 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs @@ -10133,6 +10133,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency ColumnOptionJson = DefaultColumnOptionJson(), PermissionJson = DefaultPermissionJson(listFormName), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.IntranetWidget)), + SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), + SeedSyncInsert = true, + SeedSyncUpdate = true, + SeedSyncDelete = true, DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), PagerOptionJson = DefaultPagerOptionJson, InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), diff --git a/api/src/Sozsoft.Platform.Domain.Shared/ListForms/IListFormArtifact.cs b/api/src/Sozsoft.Platform.Domain.Shared/ListForms/IListFormArtifact.cs index f640690c..5b3d1889 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/ListForms/IListFormArtifact.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/ListForms/IListFormArtifact.cs @@ -51,6 +51,18 @@ public interface IListFormArtifact string DeleteBeforeCommand { get; set; } string DeleteAfterCommand { get; set; } + /// Kapsam klasorune gore goreli seed dosyasi yolu; dolu ise liste verisi bu dosyayla senkron tutulur. + string SeedFilePath { get; set; } + + /// Ekrandan eklenen kayit seed dosyasina da eklenir. + bool SeedSyncInsert { get; set; } + + /// Ekrandan guncellenen kaydin seed dosyasindaki karsiligi guncellenir. + bool SeedSyncUpdate { get; set; } + + /// Ekrandan silinen kaydin satiri seed dosyasindan cikarilir. + bool SeedSyncDelete { get; set; } + string SelectFieldsDefaultValueJson { get; set; } string InsertFieldsDefaultValueJson { get; set; } string UpdateFieldsDefaultValueJson { get; set; } diff --git a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs index e622766c..d5abfa45 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs @@ -91,6 +91,7 @@ public static class TableNameResolver { nameof(TableNameEnum.Agenda), (TablePrefix.TenantByName, MenuPrefix.Administration) }, // 🔹 INTRANET TABLOLARI + { nameof(TableNameEnum.IntranetWidget), (TablePrefix.TenantByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.Announcement), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.AnnouncementComment), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.AnnouncementLike), (TablePrefix.TenantByName, MenuPrefix.Administration) }, @@ -111,7 +112,6 @@ public static class TableNameResolver { nameof(TableNameEnum.Event), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.EventPhoto), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.EventComment), (TablePrefix.TenantByName, MenuPrefix.Administration) }, - { nameof(TableNameEnum.IntranetWidget), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.MessengerConversation), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.MessengerConversationMessage), (TablePrefix.TenantByName, MenuPrefix.Administration) }, diff --git a/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs b/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs new file mode 100644 index 00000000..f03d3879 --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Sozsoft.Platform.DynamicData; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Enums; +using Sozsoft.Platform.ListForms; +using Sozsoft.Platform.Queries; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; + +namespace Sozsoft.Platform.Data.Seeds; + +/// +/// Kapsam klasorundeki data/*.json dosyalarini okuyup satirlari ilgili ListForm'un +/// tablosuna yazar. +/// +public interface IListFormSeedDataApplier +{ + /// Verilen kapsamdaki tum veri seed dosyalarini uygular. + Task ApplyAsync(Guid? tenantId); +} + +/// +/// Ekranlardan girilen veri ListForm.SeedFilePath ile bir seed dosyasina aynalanir; +/// bu servis ayni dosyayi ters yonde uygular. Satirlar anahtar alan uzerinden eslesir: kayit +/// yoksa eklenir, varsa dokunulmaz. Boylece her migrate calistiginda veritabanindaki guncel +/// kayitlar dosyadaki eski degerlerle ezilmez ve kopya kayit olusmaz. +/// +/// Sorgu uretimi ile yapilir; ikinci bir SQL uretici +/// yazilmaz. Yetki suzgecinden gecen ListFormFieldManager yerine alanlar dogrudan +/// repository'den okunur — seed sirasinda oturum acmis bir kullanici yoktur. +/// +/// +public class ListFormSeedDataApplier( + IRepository listFormRepository, + IRepository listFormFieldRepository, + IQueryManager queryManager, + IDefaultValueManager defaultValueManager, + IDynamicDataManager dynamicDataManager, + IConfiguration configuration, + ICurrentTenant currentTenant, + IUnitOfWorkManager unitOfWorkManager, + ILogger logger) : IListFormSeedDataApplier, ITransientDependency +{ + public async Task ApplyAsync(Guid? tenantId) + { + var scopeFolderName = SeedPathResolver.GetScopeFolderName(tenantId); + var dataPath = SeedPathResolver.GetScopePath(configuration, tenantId, SeedPathResolver.DataFolder); + + if (!Directory.Exists(dataPath)) + { + logger.LogInformation("Seeds/{Scope}/data directory not found, skipping ListFormSeedDataApplier.", scopeFolderName); + return; + } + + var jsonFiles = Directory.GetFiles(dataPath, "*.json").OrderBy(Path.GetFileName).ToArray(); + if (jsonFiles.Length == 0) + { + logger.LogInformation("No JSON files found in Seeds/{Scope}/data directory, skipping ListFormSeedDataApplier.", scopeFolderName); + return; + } + + logger.LogInformation("ListFormSeedDataApplier started for {Scope}. {Count} file(s) to be processed.", scopeFolderName, jsonFiles.Length); + + using (currentTenant.Change(tenantId)) + { + foreach (var filePath in jsonFiles) + { + // Seed contributor'larin unit of work'u kapandigi icin her dosya kendi + // unit of work'unde islenir; aksi halde DbContext disposed gelir. + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + await ApplyFileAsync(filePath); + await uow.CompleteAsync(); + } + } + + logger.LogInformation("ListFormSeedDataApplier completed for {Scope}.", scopeFolderName); + } + + private async Task ApplyFileAsync(string filePath) + { + var fileName = Path.GetFileName(filePath); + try + { + var seedFile = JsonSerializer.Deserialize( + await File.ReadAllTextAsync(filePath), + ListFormSeedDataFileDto.JsonOptions); + + var listFormCode = string.IsNullOrWhiteSpace(seedFile?.ListFormCode) + ? Path.GetFileNameWithoutExtension(filePath) + : seedFile!.ListFormCode.Trim(); + + if (seedFile?.Rows == null || seedFile.Rows.Count == 0) + { + logger.LogWarning("[{File}] No row found, skipped.", fileName); + return; + } + + var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode); + if (listForm == null) + { + logger.LogWarning("[{File}] ListForm '{ListFormCode}' not found, skipped.", fileName, listFormCode); + return; + } + + var fields = await listFormFieldRepository.GetListAsync(a => a.ListFormCode == listFormCode); + var keyFieldName = string.IsNullOrWhiteSpace(seedFile.KeyFieldName) + ? listForm.KeyFieldName + : seedFile.KeyFieldName; + + var (repository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode); + + var inserted = 0; + var skipped = 0; + + foreach (var row in seedFile.Rows) + { + if (await ApplyRowAsync(listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType)) + inserted++; + else + skipped++; + } + + logger.LogInformation("[{File}] {Inserted} row(s) inserted, {Skipped} existing row(s) skipped.", fileName, inserted, skipped); + } + catch (Exception ex) + { + // Tek bir dosyanin hatasi diger listelerin seed edilmesini engellemez. + logger.LogError(ex, "Failed to process list form data seed file: {FileName}", fileName); + } + } + + /// Satiri ekler; kayit eklendiyse true, ayni anahtarli kayit zaten varsa false doner. + private async Task ApplyRowAsync( + ListForm listForm, + List fields, + string keyFieldName, + Dictionary row, + IDynamicDataRepository repository, + string connectionString, + DataSourceTypeEnum dataSourceType) + { + var keyField = fields.FirstOrDefault(a => a.FieldName == keyFieldName); + var keyValue = QueryHelper.GetFormattedValue( + keyField?.SourceDbType ?? listForm.KeyFieldDbSourceType, + row.GetValueOrDefault(keyFieldName)); + + object[] keys = [keyValue]; + + // Var olan kayda dokunulmaz: veritabanindaki guncel deger, dosyadaki eski degerle ezilmemeli. + if (await ExistsAsync(listForm, keyFieldName, keyValue, repository, connectionString)) + { + return false; + } + + var insertParameters = BuildParameters(fields, row, keyFieldName, includeKey: true); + await MergeDefaultsAsync(listForm, fields, OperationEnum.Insert, keys, insertParameters); + + // Insert varsayilanlari anahtari @NEWID ile uretir; dosyadaki anahtar korunmazsa ayni + // dosya her uygulandiginda yeni kayit olusurdu. + insertParameters[keyFieldName] = keyValue; + + var insertSql = queryManager.GenerateQuery(listForm, fields, insertParameters, OperationEnum.Insert, dataSourceType, keys); + await repository.ExecuteAsync(insertSql, connectionString, insertParameters); + return true; + } + + /// Ayni anahtara sahip bir kayit hedef tabloda var mi. + private static async Task ExistsAsync( + ListForm listForm, + string keyFieldName, + object keyValue, + IDynamicDataRepository repository, + string connectionString) + { + var sql = $"SELECT COUNT(1) FROM \"{listForm.SelectCommand}\" WHERE \"{keyFieldName}\" = @{keyFieldName}"; + var parameters = new Dictionary { [keyFieldName] = keyValue }; + + return await repository.ExecuteScalarAsync(sql, connectionString, parameters) > 0; + } + + /// + /// Ekran tanimindaki varsayilan alanlari (audit, anahtar, tenant) parametrelere ekler — + /// ListFormManager.GetParametersAsync ile ayni sozlesme. + /// + private async Task MergeDefaultsAsync( + ListForm listForm, + List fields, + OperationEnum op, + object[] keys, + Dictionary parameters) + { + if (PlatformConsts.IsMultiTenant && listForm.IsTenant && !parameters.ContainsKey("TenantId")) + { + parameters["TenantId"] = currentTenant.Id; + } + + var defaults = await defaultValueManager.GenerateDefaultValuesAsync(listForm, fields, op, keys); + foreach (var item in defaults) + { + parameters[item.Key] = item.Value.CustomValue; + } + } + + /// Dosyadaki satiri, ekranin alan tanimlarina gore tiplendirilmis parametrelere cevirir. + private static Dictionary BuildParameters( + List fields, + Dictionary row, + string keyFieldName, + bool includeKey) + { + var parameters = new Dictionary(); + + foreach (var item in row) + { + if (!includeKey && item.Key == keyFieldName) + continue; + + var field = fields.FirstOrDefault(a => a.FieldName == item.Key); + if (field == null) + continue; + + parameters[field.FieldName] = QueryHelper.GetFormattedValue(field.SourceDbType, item.Value); + } + + return parameters; + } +} diff --git a/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataFile.cs b/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataFile.cs new file mode 100644 index 00000000..4e3dbadc --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataFile.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Sozsoft.Platform.Data.Seeds; + +/// +/// Bir ListForm ekranindan (Grid, Tree, TodoBoard vb.) girilen verinin seed dosyasi: +/// {host|tenants/{tenantId}}/data/{ListFormCode}.json. +/// +/// Ekrandaki her insert/update/delete sonrasi ilgili satir dosyaya islenir; veritabani +/// sifirlandiginda ListFormSeedDataApplier ayni dosyayi okuyup satirlari geri yazar. +/// Bir ListForm'un bu dosyayla iliskisi ListForm.SeedFilePath alanindan gelir. +/// +/// +public class ListFormSeedDataFileDto +{ + /// Verinin ait oldugu ekran; dosya adi ile ayni olmasi beklenir. + public string ListFormCode { get; set; } = string.Empty; + + /// Satirlarin eslestigi anahtar alan (ListForm.KeyFieldName). + public string KeyFieldName { get; set; } = string.Empty; + + /// Dosyanin en son yazildigi an (UTC) — yalnizca bilgi amaclidir. + public DateTime GeneratedAt { get; set; } + + /// Ekranin select alanlarindan olusan satirlar; sira dosyadaki sirayi korur. + public List> Rows { get; set; } = []; + + public static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; +} diff --git a/api/src/Sozsoft.Platform.Domain/Data/PlatformDbMigrationService.cs b/api/src/Sozsoft.Platform.Domain/Data/PlatformDbMigrationService.cs index d5efb3c9..aeaea83d 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/PlatformDbMigrationService.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/PlatformDbMigrationService.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Sozsoft.Platform.Data.Seeds; using StackExchange.Redis; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; @@ -27,6 +28,7 @@ public class PlatformDbMigrationService : ITransientDependency private readonly ICurrentTenant _currentTenant; private readonly IConfiguration _configuration; private readonly IEnumerable _afterAllMigrationsSqlExecutors; + private readonly IListFormSeedDataApplier _listFormSeedDataApplier; public PlatformDbMigrationService( IDataSeeder dataSeeder, @@ -34,7 +36,8 @@ public class PlatformDbMigrationService : ITransientDependency ITenantRepository tenantRepository, ICurrentTenant currentTenant, IConfiguration configuration, - IEnumerable afterAllMigrationsSqlExecutors) + IEnumerable afterAllMigrationsSqlExecutors, + IListFormSeedDataApplier listFormSeedDataApplier) { _dataSeeder = dataSeeder; _dbSchemaMigrators = dbSchemaMigrators; @@ -42,6 +45,7 @@ public class PlatformDbMigrationService : ITransientDependency _currentTenant = currentTenant; _configuration = configuration; _afterAllMigrationsSqlExecutors = afterAllMigrationsSqlExecutors; + _listFormSeedDataApplier = listFormSeedDataApplier; Logger = NullLogger.Instance; } @@ -177,6 +181,10 @@ public class PlatformDbMigrationService : ITransientDependency .WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, PlatformConsts.AbpIdentity.User.AdminPasswordDefaultValue) .WithProperty(IdentityDataSeedContributor.AdminUserNamePropertyName, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue) ); + + // Veri seed dosyalari ListForm tanimlarina ve tablolara dayandigi icin butun + // contributor'lar bittikten sonra uygulanir; ABP contributor sirasi garanti degildir. + await _listFormSeedDataApplier.ApplyAsync(tenant?.Id); } private bool AddInitialMigrationIfNotExist() diff --git a/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs b/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs index fbae1bd7..e6c02069 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs @@ -25,6 +25,7 @@ namespace Sozsoft.Platform.Data.Seeds; /// crud/ /// custom/ /// wizard/ +/// data/ /// sql/ → SQL Server saglayicisi /// object/ → nesne olusturulur/guncellenir /// execute/ → nesne olusturulur, ayrica tum migration'lar bitince calistirilir @@ -33,7 +34,7 @@ namespace Sozsoft.Platform.Data.Seeds; /// execute/ /// tenants/ /// {tenantId}/ → her tenant kendi klasorunde -/// crud/ custom/ wizard/ sql/{object,execute}/ postgres/{object,execute}/ +/// crud/ custom/ wizard/ data/ sql/{object,execute}/ postgres/{object,execute}/ /// /// /// @@ -74,6 +75,15 @@ public static class SeedPathResolver /// Custom component konfigurasyon dosyalari. public const string CustomFolder = "custom"; + /// Liste verisinin aynalandigi seed dosyalari (data/{ListFormCode}.json). + public const string DataFolder = "data"; + + /// Bir ListForm icin varsayilan veri seed dosyasinin kapsam klasorune gore goreli yolu. + public static string GetDataFilePath(string listFormCode) + { + return $"{DataFolder}/{listFormCode}.json"; + } + private const string RepositorySeedsFolder = "seeds"; private const string RepositoryConfigsFolder = "configs"; private const string FallbackFolderName = "Seeds"; diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListForm.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListForm.cs index 6c05065e..d5849edb 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListForm.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListForm.cs @@ -98,6 +98,22 @@ public class ListForm : Entity, IListFormArtifact /// public string DeleteServiceAddress { get; set; } = "list-form-data/delete"; + /// Bu listenin verisinin aynalandigi seed dosyasinin kapsam klasorune gore goreli yolu + /// (ornegin data/App.DeveloperKit.IntranetWidgets.json). Dolu ise ekrandan yapilan + /// seed sirasinda dosyadaki satirlar tabloya geri yuklenir. Ekranda yapilan islemlerin dosyaya + /// yansimasi asagidaki bayraklara baglidir. + /// + public string SeedFilePath { get; set; } + + /// Ekrandan eklenen kayit seed dosyasina da eklenir. + public bool SeedSyncInsert { get; set; } + + /// Ekrandan guncellenen kaydin seed dosyasindaki karsiligi guncellenir. + public bool SeedSyncUpdate { get; set; } + + /// Ekrandan silinen kaydin satiri seed dosyasindan cikarilir. + public bool SeedSyncDelete { get; set; } + public string CustomJsSourcesJson { get; set; } // Sayfa yuklendiginde calistirilacak veya cagrilacak olan JS kodlari icin public string CustomStyleSourcesJson { get; set; } // Sayfa yuklendiginde calistirilacak veya cagrilacak olan CSS kodlari icin diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index 3c490fb9..066add2a 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -368,6 +368,10 @@ public class PlatformDbContext : b.Property(a => a.UpdateServiceAddress).HasMaxLength(256); b.Property(a => a.InsertServiceAddress).HasMaxLength(256); b.Property(a => a.DeleteServiceAddress).HasMaxLength(256); + b.Property(a => a.SeedFilePath).HasMaxLength(256); + b.Property(a => a.SeedSyncInsert).HasDefaultValue(false); + b.Property(a => a.SeedSyncUpdate).HasDefaultValue(false); + b.Property(a => a.SeedSyncDelete).HasDefaultValue(false); b.Property(a => a.UserId).HasMaxLength(256); b.Property(a => a.RoleId).HasMaxLength(256); b.Property(a => a.ListFormType).HasMaxLength(20); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260903062758_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260904084415_Initial.Designer.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260903062758_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260904084415_Initial.Designer.cs index 7e5a04a7..496baf07 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260903062758_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260904084415_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260903062758_Initial")] + [Migration("20260904084415_Initial")] partial class Initial { /// @@ -3189,7 +3189,7 @@ namespace Sozsoft.Platform.Migrations .IsUnique() .HasFilter("[IsDeleted] = 0"); - b.ToTable("Adm_T_IntranetWidget", (string)null); + b.ToTable("Sas_T_IntranetWidget", (string)null); }); modelBuilder.Entity("Sozsoft.Platform.Entities.IpRestriction", b => @@ -3526,6 +3526,25 @@ namespace Sozsoft.Platform.Migrations b.Property("SearchPanelJson") .HasColumnType("nvarchar(max)"); + b.Property("SeedFilePath") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("SeedSyncDelete") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("SeedSyncInsert") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("SeedSyncUpdate") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + b.Property("SelectCommand") .HasColumnType("nvarchar(max)"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260903062758_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260904084415_Initial.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260903062758_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260904084415_Initial.cs index 25acb152..91facf3f 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260903062758_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260904084415_Initial.cs @@ -601,35 +601,6 @@ namespace Sozsoft.Platform.Migrations table.PrimaryKey("PK_Adm_T_EventType", x => x.Id); }); - migrationBuilder.CreateTable( - name: "Adm_T_IntranetWidget", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - TenantId = table.Column(type: "uniqueidentifier", nullable: true), - Code = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), - Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), - LabelKey = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), - SourceType = table.Column(type: "int", nullable: false), - ComponentName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), - PermissionName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), - Column = table.Column(type: "int", nullable: false), - Order = table.Column(type: "int", nullable: false), - Props = table.Column(type: "nvarchar(max)", nullable: true), - IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), - CreationTime = table.Column(type: "datetime2", nullable: false), - CreatorId = table.Column(type: "uniqueidentifier", nullable: true), - LastModificationTime = table.Column(type: "datetime2", nullable: true), - LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), - IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), - DeleterId = table.Column(type: "uniqueidentifier", nullable: true), - DeletionTime = table.Column(type: "datetime2", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Adm_T_IntranetWidget", x => x.Id); - }); - migrationBuilder.CreateTable( name: "Adm_T_IpRestriction", columns: table => new @@ -1531,6 +1502,10 @@ namespace Sozsoft.Platform.Migrations UpdateServiceAddress = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), InsertServiceAddress = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), DeleteServiceAddress = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + SeedFilePath = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + SeedSyncInsert = table.Column(type: "bit", nullable: false, defaultValue: false), + SeedSyncUpdate = table.Column(type: "bit", nullable: false, defaultValue: false), + SeedSyncDelete = table.Column(type: "bit", nullable: false, defaultValue: false), CustomJsSourcesJson = table.Column(type: "nvarchar(max)", nullable: true), CustomStyleSourcesJson = table.Column(type: "nvarchar(max)", nullable: true), FormFieldsDefaultValueJson = table.Column(type: "nvarchar(max)", nullable: true), @@ -1943,6 +1918,35 @@ namespace Sozsoft.Platform.Migrations table.PrimaryKey("PK_Sas_T_GlobalSearch", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Sas_T_IntranetWidget", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + Code = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + LabelKey = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + SourceType = table.Column(type: "int", nullable: false), + ComponentName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + PermissionName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Column = table.Column(type: "int", nullable: false), + Order = table.Column(type: "int", nullable: false), + Props = table.Column(type: "nvarchar(max)", nullable: true), + IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), + CreationTime = table.Column(type: "datetime2", nullable: false), + CreatorId = table.Column(type: "uniqueidentifier", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uniqueidentifier", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Sas_T_IntranetWidget", x => x.Id); + }); + migrationBuilder.CreateTable( name: "AbpAuditLogActions", columns: table => new @@ -3867,13 +3871,6 @@ namespace Sozsoft.Platform.Migrations unique: true, filter: "[IsDeleted] = 0"); - migrationBuilder.CreateIndex( - name: "IX_Adm_T_IntranetWidget_TenantId_Code", - table: "Adm_T_IntranetWidget", - columns: new[] { "TenantId", "Code" }, - unique: true, - filter: "[IsDeleted] = 0"); - migrationBuilder.CreateIndex( name: "IX_Adm_T_IpRestriction_TenantId_ResourceType_ResourceId_IP", table: "Adm_T_IpRestriction", @@ -4406,6 +4403,13 @@ namespace Sozsoft.Platform.Migrations name: "IX_Sas_T_ForumTopic_CategoryId", table: "Sas_T_ForumTopic", column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Sas_T_IntranetWidget_TenantId_Code", + table: "Sas_T_IntranetWidget", + columns: new[] { "TenantId", "Code" }, + unique: true, + filter: "[IsDeleted] = 0"); } /// @@ -4501,9 +4505,6 @@ namespace Sozsoft.Platform.Migrations migrationBuilder.DropTable( name: "Adm_T_EventLike"); - migrationBuilder.DropTable( - name: "Adm_T_IntranetWidget"); - migrationBuilder.DropTable( name: "Adm_T_IpRestriction"); @@ -4684,6 +4685,9 @@ namespace Sozsoft.Platform.Migrations migrationBuilder.DropTable( name: "Sas_T_GlobalSearch"); + migrationBuilder.DropTable( + name: "Sas_T_IntranetWidget"); + migrationBuilder.DropTable( name: "AbpEntityChanges"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index 8414971b..3bbab29a 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -3186,7 +3186,7 @@ namespace Sozsoft.Platform.Migrations .IsUnique() .HasFilter("[IsDeleted] = 0"); - b.ToTable("Adm_T_IntranetWidget", (string)null); + b.ToTable("Sas_T_IntranetWidget", (string)null); }); modelBuilder.Entity("Sozsoft.Platform.Entities.IpRestriction", b => @@ -3523,6 +3523,25 @@ namespace Sozsoft.Platform.Migrations b.Property("SearchPanelJson") .HasColumnType("nvarchar(max)"); + b.Property("SeedFilePath") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("SeedSyncDelete") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("SeedSyncInsert") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("SeedSyncUpdate") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + b.Property("SelectCommand") .HasColumnType("nvarchar(max)"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json index 1cffd581..af14f329 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json @@ -2239,91 +2239,5 @@ "FormatTemplate": "{Prefix}-{yyyy}{MM}{dd}-{Number}", "ResetPeriod": "Daily" } - ], - "Widgets": [ - { - "Code": "today-birthdays", - "Name": "Today Birthdays", - "LabelKey": "::App.DashboardWidget.Birthdays", - "SourceType": "Builtin", - "ComponentName": "", - "PermissionName": "AbpIdentity.Users.Widget", - "Column": "Left", - "Order": 10, - "Props": "", - "IsActive": true - }, - { - "Code": "documents", - "Name": "Documents", - "LabelKey": "::App.Intranet.Documents", - "SourceType": "Builtin", - "ComponentName": "", - "PermissionName": "App.Files.Widget", - "Column": "Left", - "Order": 20, - "Props": "", - "IsActive": true - }, - { - "Code": "active-surveys", - "Name": "Active Surveys", - "LabelKey": "::App.DashboardWidget.Surveys", - "SourceType": "Builtin", - "ComponentName": "", - "PermissionName": "App.Intranet.Survey.Widget", - "Column": "Left", - "Order": 30, - "Props": "", - "IsActive": true - }, - { - "Code": "social-wall", - "Name": "Social Wall", - "LabelKey": "::App.DashboardWidget.SocialWall", - "SourceType": "Builtin", - "ComponentName": "", - "PermissionName": "App.Intranet.SocialPost.Widget", - "Column": "Center", - "Order": 40, - "Props": "", - "IsActive": true - }, - { - "Code": "announcements", - "Name": "Announcements", - "LabelKey": "::App.Intranet.Announcement", - "SourceType": "Builtin", - "ComponentName": "", - "PermissionName": "App.Intranet.Announcement.Widget", - "Column": "Right", - "Order": 50, - "Props": "", - "IsActive": true - }, - { - "Code": "upcoming-events", - "Name": "Upcoming Events", - "LabelKey": "::App.DashboardWidget.Events", - "SourceType": "Builtin", - "ComponentName": "", - "PermissionName": "App.Intranet.Events.Event.Widget", - "Column": "Right", - "Order": 60, - "Props": "", - "IsActive": true - }, - { - "Code": "role-list", - "Name": "Role List", - "LabelKey": "::AbpIdentity.Roles", - "SourceType": "Custom", - "ComponentName": "RoleComponent", - "PermissionName": "AbpIdentity.Roles", - "Column": "Center", - "Order": 20, - "Props": "", - "IsActive": true - } ] } diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs index 416595e2..ebd64173 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs @@ -69,7 +69,6 @@ public class TenantSeederDto public List Events { get; set; } public List EventPhotos { get; set; } public List EventComments { get; set; } - public List Widgets { get; set; } } public class SequenceSeedDto @@ -196,20 +195,6 @@ public class SurveyQuestionOptionSeedDto public int Order { get; set; } } -public class IntranetWidgetSeedDto -{ - public string Code { get; set; } - public string Name { get; set; } - public string LabelKey { get; set; } - public IntranetWidgetSourceType SourceType { get; set; } - public string ComponentName { get; set; } - public string PermissionName { get; set; } - public IntranetWidgetColumn Column { get; set; } - public int Order { get; set; } - public string Props { get; set; } - public bool IsActive { get; set; } = true; -} - public class AnnouncementSeedDto { public string Title { get; set; } @@ -502,7 +487,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency private readonly IRepository _departmentRepository; private readonly IRepository _jobPositionRepository; private readonly IRepository _announcementRepository; - private readonly IRepository _intranetWidgetRepository; private readonly IRepository _surveyRepository; private readonly IRepository _surveyQuestionRepository; private readonly IRepository _surveyQuestionOptionRepository; @@ -547,7 +531,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency IRepository jobPositionRepository, IRepository announcementRepository, - IRepository intranetWidgetRepository, IRepository surveyRepository, IRepository surveyQuestionRepository, IRepository surveyQuestionOptionRepository, @@ -589,7 +572,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency _departmentRepository = departmentRepository; _jobPositionRepository = jobPositionRepository; _announcementRepository = announcementRepository; - _intranetWidgetRepository = intranetWidgetRepository; _surveyRepository = surveyRepository; _surveyQuestionRepository = surveyQuestionRepository; _surveyQuestionOptionRepository = surveyQuestionOptionRepository; @@ -1024,26 +1006,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency }, autoSave: true); } - foreach (var item in items.Widgets) - { - if (await _intranetWidgetRepository.AnyAsync(x => x.Code == item.Code)) - continue; - - await _intranetWidgetRepository.InsertAsync(new IntranetWidget - { - Code = item.Code, - Name = item.Name, - LabelKey = item.LabelKey, - SourceType = item.SourceType, - ComponentName = item.ComponentName, - PermissionName = item.PermissionName, - Column = item.Column, - Order = item.Order, - Props = item.Props, - IsActive = item.IsActive - }); - } - foreach (var item in items.Announcements) { var exists = await _announcementRepository.AnyAsync(x => x.Title == item.Title); diff --git a/claude.md b/claude.md index c90c5170..97f4b5f5 100644 --- a/claude.md +++ b/claude.md @@ -188,6 +188,11 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → `crud/{Entity}.json`. - **Wizard dosyası** → tek başına taşınmaz; export zip'i `wizard/`, `custom/`, `crud/` ve `{sql|postgres}/{object|execute}/` bağımlılıklarını da içerir. +- **`ListForm.SeedFilePath` dolu bir ekran** → `SeedSyncInsert/SeedSyncUpdate/SeedSyncDelete` + bayraklarından işaretli olan işlemler kapsam klasöründeki `data/{ListFormCode}.json` dosyasına + yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında `ListFormSeedDataApplier` dosyayı + geri uygular (kayıt yoksa ekler, varsa dokunmaz). Yeni bir kayıt yolu (yeni endpoint, toplu işlem) + eklenirse senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar. - **`MenusData.json`** → `Routes` bölümüne dokunma; yalnızca `Modules` ve `Menus`. - **Çok değerli alan (`dxTagBox` / çoklu `dxGridBox`)** → tek metin kolonunda `|` ile saklanır. Ekran varsayılan `list-form-data/*` ucundan kaydediyorsa birleştirmeyi `QueryHelper` yapar; @@ -205,6 +210,14 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → `IDE00xx` yok. - `ui/` değişikliğinden sonra: `npm run typecheck` ve `npm run lint`. - Yorumlar **neden**i anlatır, ne yaptığını değil. Kod zaten ne yaptığını söylüyor. +- **Dokümantasyon kodla birlikte güncellenir.** Platformun davranışını değiştiren her değişiklik + (yeni/kaldırılan seeder, seed klasörü ya da dosya biçimi, ListForm sözleşmesine alan ekleme, + yeni yetki/dil anahtarı, katman sınırı, yeni mekanizma) aynı commit içinde ilgili `.md` + dosyasına yansıtılır: `README.md` (ne nerede yaşıyor, klasör düzeni, tablolar), + `.github/instructions/lowcode*.instructions.md` (artefakt/yetenek sözleşmesi), + `dotnet.instructions.md` (kod standardı, katman kuralı), `CLAUDE.md` (çalışma kuralı). + Değişiklik hangi md'de karşılığı olduğu bilinmiyorsa önce grep ile eski anlatım aranır ve + düzeltilir; bayat kalan bir satır bırakma. Doküman güncellemesi "sonraya" bırakılmaz. ### Tone diff --git a/configs/seeds/host/data/App.DeveloperKit.IntranetWidgets.json b/configs/seeds/host/data/App.DeveloperKit.IntranetWidgets.json new file mode 100644 index 00000000..6534eb53 --- /dev/null +++ b/configs/seeds/host/data/App.DeveloperKit.IntranetWidgets.json @@ -0,0 +1,85 @@ +{ + "ListFormCode": "App.DeveloperKit.IntranetWidgets", + "KeyFieldName": "Id", + "GeneratedAt": "2026-09-04T00:00:00Z", + "Rows": [ + { + "Id": "542e0ab0-1756-5ca8-8806-72d00361ff8f", + "Code": "today-birthdays", + "Name": "Today Birthdays", + "LabelKey": "App.DashboardWidget.Birthdays", + "SourceType": 0, + "ComponentName": "", + "PermissionName": "AbpIdentity.Users.Widget", + "Column": 0, + "Order": 10, + "Props": "", + "IsActive": true + }, + { + "Id": "f0c19fb9-6817-5401-99db-64f77e970b1e", + "Code": "documents", + "Name": "Documents", + "LabelKey": "App.Intranet.Documents", + "SourceType": 0, + "ComponentName": "", + "PermissionName": "App.Files.Widget", + "Column": 0, + "Order": 20, + "Props": "", + "IsActive": true + }, + { + "Id": "d0db4db6-7fc7-511c-97ea-89b9e998e62f", + "Code": "active-surveys", + "Name": "Active Surveys", + "LabelKey": "App.DashboardWidget.Surveys", + "SourceType": 0, + "ComponentName": "", + "PermissionName": "App.Intranet.Survey.Widget", + "Column": 0, + "Order": 30, + "Props": "", + "IsActive": true + }, + { + "Id": "1890e376-77cd-5c2f-8284-ff027d62c548", + "Code": "social-wall", + "Name": "Social Wall", + "LabelKey": "App.DashboardWidget.SocialWall", + "SourceType": 0, + "ComponentName": "", + "PermissionName": "App.Intranet.SocialPost.Widget", + "Column": 1, + "Order": 40, + "Props": "", + "IsActive": true + }, + { + "Id": "6ea6a7ca-a4ec-58c9-b049-dc9fccca9256", + "Code": "announcements", + "Name": "Announcements", + "LabelKey": "App.Intranet.Announcement", + "SourceType": 0, + "ComponentName": "", + "PermissionName": "App.Intranet.Announcement.Widget", + "Column": 2, + "Order": 50, + "Props": "", + "IsActive": true + }, + { + "Id": "674e05fe-938c-5cd5-ac2d-6428d236bb56", + "Code": "upcoming-events", + "Name": "Upcoming Events", + "LabelKey": "App.DashboardWidget.Events", + "SourceType": 0, + "ComponentName": "", + "PermissionName": "App.Intranet.Events.Event.Widget", + "Column": 2, + "Order": 60, + "Props": "", + "IsActive": true + } + ] +} diff --git a/ui/src/proxy/form/models.ts b/ui/src/proxy/form/models.ts index 9e116582..2d0babff 100644 --- a/ui/src/proxy/form/models.ts +++ b/ui/src/proxy/form/models.ts @@ -591,6 +591,14 @@ export interface GridOptionsDto extends AuditedEntityDto { updateServiceAddress?: string insertServiceAddress?: string deleteServiceAddress?: string + /** Kapsam klasörüne göre göreli seed dosyası yolu; dolu ise liste verisi bu dosyayla senkron tutulur. */ + seedFilePath?: string + /** Ekrandan eklenen kayıt seed dosyasına da eklenir. */ + seedSyncInsert?: boolean + /** Ekrandan güncellenen kaydın seed dosyasındaki karşılığı güncellenir. */ + seedSyncUpdate?: boolean + /** Ekrandan silinen kaydın satırı seed dosyasından çıkarılır. */ + seedSyncDelete?: boolean isTenant: boolean isBranch: boolean isOrganizationUnit: boolean diff --git a/ui/src/views/admin/listForm/edit/FormTabDatabaseDataSource.tsx b/ui/src/views/admin/listForm/edit/FormTabDatabaseDataSource.tsx index 799e6449..cb8c4e91 100644 --- a/ui/src/views/admin/listForm/edit/FormTabDatabaseDataSource.tsx +++ b/ui/src/views/admin/listForm/edit/FormTabDatabaseDataSource.tsx @@ -25,8 +25,16 @@ const schema = Yup.object().shape({ tableName: Yup.string(), keyFieldName: Yup.string(), keyFieldDbSourceType: Yup.string(), + seedFilePath: Yup.string(), + seedSyncInsert: Yup.bool(), + seedSyncUpdate: Yup.bool(), + seedSyncDelete: Yup.bool(), }) +// Kapsam ve seed senkronizasyon bayraklari tek satirda yan yana gosterilir. +const scopeFields = ['isTenant', 'isBranch', 'isOrganizationUnit'] as const +const seedSyncFields = ['seedSyncInsert', 'seedSyncUpdate', 'seedSyncDelete'] as const + function FormTabDatabaseDataSource(props: FormEditProps) { const initialValues = useStoreState((s) => s.admin.lists.values) const { translate } = useLocalization() @@ -143,41 +151,21 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
- - - - - - - - + +
+ {scopeFields.map((name) => ( + + {({ field, form }: FieldProps) => ( + form.setFieldValue(field.name, checked)} + > + {translate(`::App.ListFormEdit.${name[0].toUpperCase()}${name.slice(1)}`)} + + )} + + ))} +
+ + {translate('::App.ListFormEdit.SeedFilePathHint')} + + } + > + + + +
+ {seedSyncFields.map((name) => ( + + {({ field, form }: FieldProps) => ( + form.setFieldValue(field.name, checked)} + > + {translate(`::App.ListFormEdit.${name[0].toUpperCase()}${name.slice(1)}`)} + + )} + + ))} +
+