seed/data Seeder düzenlemesi

This commit is contained in:
Sedat ÖZTÜRK 2026-09-04 12:05:26 +03:00
parent e237cacc2a
commit 9e99475a3e
28 changed files with 1005 additions and 218 deletions

View file

@ -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ç"

2
.gitignore vendored
View file

@ -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

View file

@ -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ü. |

View file

@ -32,6 +32,19 @@ public class GridOptionsEditDto : GridOptionsDto
set { SelectFieldsDefaultValueJson = JsonSerializer.Serialize(value); }
}
/// <summary>Kapsam klasorune gore goreli seed dosyasi yolu (ornegin <c>data/{ListFormCode}.json</c>).
/// Dolu ise ekrandan yapilan kayit islemleri dosyaya yansitilir.</summary>
public string SeedFilePath { get; set; }
/// <summary>Ekrandan eklenen kayit seed dosyasina da eklenir.</summary>
public bool SeedSyncInsert { get; set; }
/// <summary>Ekrandan guncellenen kaydin seed dosyasindaki karsiligi guncellenir.</summary>
public bool SeedSyncUpdate { get; set; }
/// <summary>Ekrandan silinen kaydin satiri seed dosyasindan cikarilir.</summary>
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;

View file

@ -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)
{

View file

@ -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<dynamic>(input.ListFormCode, OperationEnum.Insert, input.Data, queryParameters: queryParameters);
var insertedKey = await qManager.GenerateAndRunQueryAsync<dynamic>(input.ListFormCode, OperationEnum.Insert, input.Data, queryParameters: queryParameters);
await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, (object)insertedKey, SeedSyncOperation.Insert);
return insertedKey;
}
public async Task<dynamic> PostDuplicateAsync(DataRequestDto input)
@ -74,7 +81,11 @@ public class ListFormDataAppService : PlatformAppService
input.Data = JsonSerializer.Serialize(dict);
return await qManager.GenerateAndRunQueryAsync<dynamic>(input.ListFormCode, OperationEnum.Duplicate, input.Data, null, queryParameters);
var duplicatedKey = await qManager.GenerateAndRunQueryAsync<dynamic>(input.ListFormCode, OperationEnum.Duplicate, input.Data, null, queryParameters);
await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, (object)duplicatedKey, SeedSyncOperation.Insert);
return duplicatedKey;
}
public async Task<int> 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<int>(input.ListFormCode, OperationEnum.Update, input.Data, input.Keys, queryParameters);
var affected = await qManager.GenerateAndRunQueryAsync<int>(input.ListFormCode, OperationEnum.Update, input.Data, input.Keys, queryParameters);
await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, input.Keys?.FirstOrDefault(), SeedSyncOperation.Update);
return affected;
}
public async Task<int> 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<int>(input.ListFormCode, OperationEnum.Delete, input.Data, input.Keys, queryParameters);
var affected = await qManager.GenerateAndRunQueryAsync<int>(input.ListFormCode, OperationEnum.Delete, input.Data, input.Keys, queryParameters);
await seedDataSynchronizer.RemoveRowsAsync(input.ListFormCode, input.Keys);
return affected;
}
}

View file

@ -35,6 +35,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
private readonly IConfiguration configuration;
private readonly IOptions<IdentityOptions> identityOptions;
private readonly IRepository<Order, Guid> orderRepository;
private readonly IListFormSeedDataSynchronizer seedDataSynchronizer;
public ListFormDynamicApiAppService(
ITenantRepository tenantRepository,
@ -44,7 +45,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
BlobManager blobCdnManager,
IConfiguration configuration,
IOptions<IdentityOptions> identityOptions,
IRepository<Order, Guid> orderRepository)
IRepository<Order, Guid> 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<CreateUpdateRoleInput> 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<CreateUpdateTenantInput> 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);
}
}

View file

@ -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<ListFormImport, Guid> importSessionRepository,
IRepository<ListFormImportLog, Guid> 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<ListFormsImportDto> 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<ListFormImportLog, ListFormImportLogDto>(execute);
}
catch (Exception ex)

View file

@ -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;
/// <summary>
/// Ekrandan yapilan kayit islemlerini, ListForm'un bagli oldugu veri seed dosyasina yansitir.
/// </summary>
public interface IListFormSeedDataSynchronizer
{
/// <summary>
/// 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.
/// </summary>
Task SyncRowAsync(string listFormCode, object key, SeedSyncOperation operation);
/// <summary>Silinen satirlari dosyadan cikarir; <c>SeedSyncDelete</c> kapaliysa hicbir sey yapmaz.</summary>
Task RemoveRowsAsync(string listFormCode, object[] keys);
/// <summary>
/// Dosyayi listenin tamamiyla yeniden yazar; toplu islemler (import) icin kullanilir.
/// Toplu yukleme kayit ekledigi icin <c>SeedSyncInsert</c> bayragina baglidir.
/// </summary>
Task SyncAllAsync(string listFormCode);
}
/// <summary>Senkronizasyonu tetikleyen islem; ekranin hangi bayraginin bakilacagini belirler.</summary>
public enum SeedSyncOperation
{
Insert,
Update
}
/// <summary>
/// <c>ListForm.SeedFilePath</c> dolu olan ekranlarin verisi kapsam klasorundeki bir JSON dosyasinda
/// aynalanir; veritabani sifirlandiginda <c>ListFormSeedDataApplier</c> 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.
/// </summary>
public class ListFormSeedDataSynchronizer(
IRepository<ListForm, Guid> listFormRepository,
IListFormSelectAppService listFormSelectAppService,
IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<ListFormSeedDataSynchronizer> 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<ListForm, bool> IsEnabled(SeedSyncOperation operation)
{
return operation == SeedSyncOperation.Insert
? listForm => listForm.SeedSyncInsert
: listForm => listForm.SeedSyncUpdate;
}
/// <summary>Dosyayi listenin tamamiyla yeniden yazar.</summary>
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)
});
}
/// <summary>
/// Seed dosyasi bagli degilse ya da islemin bayragi kapaliysa hicbir sey yapmaz.
/// Senkronizasyon hatasi kullanicinin kayit islemini bozmaz; yalnizca loglanir.
/// </summary>
private async Task ExecuteAsync(
string listFormCode,
Func<ListForm, bool> isEnabled,
Func<ListForm, string, Task> 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<Dictionary<string, object>> 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<Dictionary<string, object>> rows = ToRows((object)result?.Data);
return rows.FirstOrDefault();
}
/// <summary>Select sonucundaki dinamik satirlari dosyaya yazilabilir sozluklere cevirir.</summary>
private static List<Dictionary<string, object>> ToRows(object data)
{
var rows = new List<Dictionary<string, object>>();
if (data is not IEnumerable items)
return rows;
foreach (var item in items)
{
if (item is IDictionary<string, object> dictionary)
rows.Add(new Dictionary<string, object>(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));
}
/// <summary>Anahtarlar dosyada JsonElement, istekte string/Guid olarak gelir; metin uzerinden karsilastirilir.</summary>
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<ListFormSeedDataFileDto> ReadSeedFileAsync(string filePath, ListForm listForm)
{
var seedFile = File.Exists(filePath)
? JsonSerializer.Deserialize<ListFormSeedDataFileDto>(
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));
}
}

View file

@ -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",

View file

@ -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(),

View file

@ -51,6 +51,18 @@ public interface IListFormArtifact
string DeleteBeforeCommand { get; set; }
string DeleteAfterCommand { get; set; }
/// <summary>Kapsam klasorune gore goreli seed dosyasi yolu; dolu ise liste verisi bu dosyayla senkron tutulur.</summary>
string SeedFilePath { get; set; }
/// <summary>Ekrandan eklenen kayit seed dosyasina da eklenir.</summary>
bool SeedSyncInsert { get; set; }
/// <summary>Ekrandan guncellenen kaydin seed dosyasindaki karsiligi guncellenir.</summary>
bool SeedSyncUpdate { get; set; }
/// <summary>Ekrandan silinen kaydin satiri seed dosyasindan cikarilir.</summary>
bool SeedSyncDelete { get; set; }
string SelectFieldsDefaultValueJson { get; set; }
string InsertFieldsDefaultValueJson { get; set; }
string UpdateFieldsDefaultValueJson { get; set; }

View file

@ -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) },

View file

@ -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;
/// <summary>
/// Kapsam klasorundeki <c>data/*.json</c> dosyalarini okuyup satirlari ilgili ListForm'un
/// tablosuna yazar.
/// </summary>
public interface IListFormSeedDataApplier
{
/// <summary>Verilen kapsamdaki tum veri seed dosyalarini uygular.</summary>
Task ApplyAsync(Guid? tenantId);
}
/// <summary>
/// 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
/// yoksa eklenir, varsa dokunulmaz. Boylece her migrate calistiginda veritabanindaki guncel
/// kayitlar dosyadaki eski degerlerle ezilmez ve kopya kayit olusmaz.
/// <para>
/// Sorgu uretimi <see cref="IQueryManager.GenerateQuery"/> ile yapilir; ikinci bir SQL uretici
/// yazilmaz. Yetki suzgecinden gecen <c>ListFormFieldManager</c> yerine alanlar dogrudan
/// repository'den okunur — seed sirasinda oturum acmis bir kullanici yoktur.
/// </para>
/// </summary>
public class ListFormSeedDataApplier(
IRepository<ListForm, Guid> listFormRepository,
IRepository<ListFormField, Guid> listFormFieldRepository,
IQueryManager queryManager,
IDefaultValueManager defaultValueManager,
IDynamicDataManager dynamicDataManager,
IConfiguration configuration,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
ILogger<ListFormSeedDataApplier> 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<ListFormSeedDataFileDto>(
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);
}
}
/// <summary>Satiri ekler; kayit eklendiyse <c>true</c>, ayni anahtarli kayit zaten varsa <c>false</c> doner.</summary>
private async Task<bool> ApplyRowAsync(
ListForm listForm,
List<ListFormField> fields,
string keyFieldName,
Dictionary<string, object> 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;
}
/// <summary>Ayni anahtara sahip bir kayit hedef tabloda var mi.</summary>
private static async Task<bool> 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<string, object> { [keyFieldName] = keyValue };
return await repository.ExecuteScalarAsync<int>(sql, connectionString, parameters) > 0;
}
/// <summary>
/// Ekran tanimindaki varsayilan alanlari (audit, anahtar, tenant) parametrelere ekler —
/// <c>ListFormManager.GetParametersAsync</c> ile ayni sozlesme.
/// </summary>
private async Task MergeDefaultsAsync(
ListForm listForm,
List<ListFormField> fields,
OperationEnum op,
object[] keys,
Dictionary<string, object> 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;
}
}
/// <summary>Dosyadaki satiri, ekranin alan tanimlarina gore tiplendirilmis parametrelere cevirir.</summary>
private static Dictionary<string, object> BuildParameters(
List<ListFormField> fields,
Dictionary<string, object> row,
string keyFieldName,
bool includeKey)
{
var parameters = new Dictionary<string, object>();
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;
}
}

View file

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Bir ListForm ekranindan (Grid, Tree, TodoBoard vb.) girilen verinin seed dosyasi:
/// <c>{host|tenants/{tenantId}}/data/{ListFormCode}.json</c>.
/// <para>
/// Ekrandaki her insert/update/delete sonrasi ilgili satir dosyaya islenir; veritabani
/// sifirlandiginda <c>ListFormSeedDataApplier</c> ayni dosyayi okuyup satirlari geri yazar.
/// Bir ListForm'un bu dosyayla iliskisi <c>ListForm.SeedFilePath</c> alanindan gelir.
/// </para>
/// </summary>
public class ListFormSeedDataFileDto
{
/// <summary>Verinin ait oldugu ekran; dosya adi ile ayni olmasi beklenir.</summary>
public string ListFormCode { get; set; } = string.Empty;
/// <summary>Satirlarin eslestigi anahtar alan (ListForm.KeyFieldName).</summary>
public string KeyFieldName { get; set; } = string.Empty;
/// <summary>Dosyanin en son yazildigi an (UTC) — yalnizca bilgi amaclidir.</summary>
public DateTime GeneratedAt { get; set; }
/// <summary>Ekranin select alanlarindan olusan satirlar; sira dosyadaki sirayi korur.</summary>
public List<Dictionary<string, object>> Rows { get; set; } = [];
public static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
}

View file

@ -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<IAfterAllMigrationsSqlExecutor> _afterAllMigrationsSqlExecutors;
private readonly IListFormSeedDataApplier _listFormSeedDataApplier;
public PlatformDbMigrationService(
IDataSeeder dataSeeder,
@ -34,7 +36,8 @@ public class PlatformDbMigrationService : ITransientDependency
ITenantRepository tenantRepository,
ICurrentTenant currentTenant,
IConfiguration configuration,
IEnumerable<IAfterAllMigrationsSqlExecutor> afterAllMigrationsSqlExecutors)
IEnumerable<IAfterAllMigrationsSqlExecutor> 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<PlatformDbMigrationService>.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()

View file

@ -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}/
/// </code>
/// </para>
/// <para>
@ -74,6 +75,15 @@ public static class SeedPathResolver
/// <summary>Custom component konfigurasyon dosyalari.</summary>
public const string CustomFolder = "custom";
/// <summary>Liste verisinin aynalandigi seed dosyalari (<c>data/{ListFormCode}.json</c>).</summary>
public const string DataFolder = "data";
/// <summary>Bir ListForm icin varsayilan veri seed dosyasinin kapsam klasorune gore goreli yolu.</summary>
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";

View file

@ -98,6 +98,22 @@ public class ListForm : Entity<Guid>, IListFormArtifact
/// </summary>
public string DeleteServiceAddress { get; set; } = "list-form-data/delete";
/// <summary> Bu listenin verisinin aynalandigi seed dosyasinin kapsam klasorune gore goreli yolu
/// (ornegin <c>data/App.DeveloperKit.IntranetWidgets.json</c>). Dolu ise ekrandan yapilan
/// seed sirasinda dosyadaki satirlar tabloya geri yuklenir. Ekranda yapilan islemlerin dosyaya
/// yansimasi asagidaki bayraklara baglidir.
/// </summary>
public string SeedFilePath { get; set; }
/// <summary>Ekrandan eklenen kayit seed dosyasina da eklenir.</summary>
public bool SeedSyncInsert { get; set; }
/// <summary>Ekrandan guncellenen kaydin seed dosyasindaki karsiligi guncellenir.</summary>
public bool SeedSyncUpdate { get; set; }
/// <summary>Ekrandan silinen kaydin satiri seed dosyasindan cikarilir.</summary>
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

View file

@ -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);

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260903062758_Initial")]
[Migration("20260904084415_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -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<string>("SearchPanelJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("SeedFilePath")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("SeedSyncDelete")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("SeedSyncInsert")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("SeedSyncUpdate")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<string>("SelectCommand")
.HasColumnType("nvarchar(max)");

View file

@ -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<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Code = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
LabelKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
SourceType = table.Column<int>(type: "int", nullable: false),
ComponentName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
PermissionName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Column = table.Column<int>(type: "int", nullable: false),
Order = table.Column<int>(type: "int", nullable: false),
Props = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(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<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
InsertServiceAddress = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
DeleteServiceAddress = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
SeedFilePath = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
SeedSyncInsert = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
SeedSyncUpdate = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
SeedSyncDelete = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
CustomJsSourcesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CustomStyleSourcesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
FormFieldsDefaultValueJson = table.Column<string>(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<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Code = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
LabelKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
SourceType = table.Column<int>(type: "int", nullable: false),
ComponentName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
PermissionName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Column = table.Column<int>(type: "int", nullable: false),
Order = table.Column<int>(type: "int", nullable: false),
Props = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(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");
}
/// <inheritdoc />
@ -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");

View file

@ -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<string>("SearchPanelJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("SeedFilePath")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("SeedSyncDelete")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("SeedSyncInsert")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("SeedSyncUpdate")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<string>("SelectCommand")
.HasColumnType("nvarchar(max)");

View file

@ -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
}
]
}

View file

@ -69,7 +69,6 @@ public class TenantSeederDto
public List<EventSeedDto> Events { get; set; }
public List<EventPhotoSeedDto> EventPhotos { get; set; }
public List<EventCommentSeedDto> EventComments { get; set; }
public List<IntranetWidgetSeedDto> 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<Department, Guid> _departmentRepository;
private readonly IRepository<JobPosition, Guid> _jobPositionRepository;
private readonly IRepository<Announcement, Guid> _announcementRepository;
private readonly IRepository<IntranetWidget, Guid> _intranetWidgetRepository;
private readonly IRepository<Survey, Guid> _surveyRepository;
private readonly IRepository<SurveyQuestion, Guid> _surveyQuestionRepository;
private readonly IRepository<SurveyQuestionOption, Guid> _surveyQuestionOptionRepository;
@ -547,7 +531,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
IRepository<JobPosition, Guid> jobPositionRepository,
IRepository<Announcement, Guid> announcementRepository,
IRepository<IntranetWidget, Guid> intranetWidgetRepository,
IRepository<Survey, Guid> surveyRepository,
IRepository<SurveyQuestion, Guid> surveyQuestionRepository,
IRepository<SurveyQuestionOption, Guid> 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);

View file

@ -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

View file

@ -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
}
]
}

View file

@ -591,6 +591,14 @@ export interface GridOptionsDto extends AuditedEntityDto<string> {
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

View file

@ -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) {
<Form>
<FormContainer size="sm">
<Card className="mt-2 mb-2">
<FormItem
label={translate('::App.ListFormEdit.IsTenant')}
invalid={errors.editingOptionDto?.isTenant && touched.editingOptionDto?.isTenant}
errorMessage={errors.editingOptionDto?.isTenant}
>
<Field
name="isTenant"
placeholder={translate('::App.ListFormEdit.IsTenant')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.IsBranch')}
invalid={errors.editingOptionDto?.isBranch && touched.editingOptionDto?.isBranch}
errorMessage={errors.editingOptionDto?.isBranch}
>
<Field
name="isBranch"
placeholder={translate('::App.ListFormEdit.IsBranch')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.IsOrganizationUnit')}
invalid={
errors.editingOptionDto?.isOrganizationUnit &&
touched.editingOptionDto?.isOrganizationUnit
}
errorMessage={errors.editingOptionDto?.isOrganizationUnit}
>
<Field
name="isOrganizationUnit"
placeholder={translate('::App.ListFormEdit.IsOrganizationUnit')}
component={Checkbox}
/>
<FormItem label={translate('::App.ListFormEdit.ScopeOptions')}>
<div className="flex flex-wrap gap-6">
{scopeFields.map((name) => (
<Field key={name} name={name}>
{({ field, form }: FieldProps<boolean>) => (
<Checkbox
checked={field.value === true}
onChange={(checked) => form.setFieldValue(field.name, checked)}
>
{translate(`::App.ListFormEdit.${name[0].toUpperCase()}${name.slice(1)}`)}
</Checkbox>
)}
</Field>
))}
</div>
</FormItem>
<FormItem
label={translate('::App.Listform.ListformField.DataSourceCode')}
@ -403,6 +391,40 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
)}
</Field>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.SeedFilePath')}
invalid={errors.seedFilePath && touched.seedFilePath}
errorMessage={errors.seedFilePath}
extra={
<span className="text-xs ml-2 text-gray-400">
{translate('::App.ListFormEdit.SeedFilePathHint')}
</span>
}
>
<Field
type="text"
autoComplete="off"
name="seedFilePath"
placeholder="data/{ListFormCode}.json"
component={Input}
/>
</FormItem>
<FormItem label={translate('::App.ListFormEdit.SeedSyncOperations')}>
<div className="flex flex-wrap gap-6">
{seedSyncFields.map((name) => (
<Field key={name} name={name}>
{({ field, form }: FieldProps<boolean>) => (
<Checkbox
checked={field.value === true}
onChange={(checked) => form.setFieldValue(field.name, checked)}
>
{translate(`::App.ListFormEdit.${name[0].toUpperCase()}${name.slice(1)}`)}
</Checkbox>
)}
</Field>
))}
</div>
</FormItem>
</Card>
<Button block size="sm" variant="solid" loading={isSubmitting}>