seed/data Seeder düzenlemesi
This commit is contained in:
parent
e237cacc2a
commit
9e99475a3e
28 changed files with 1005 additions and 218 deletions
42
.github/instructions/lowcode.instructions.md
vendored
42
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -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
2
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
11
README.md
11
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ü. |
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
||||
|
|
|
|||
236
api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs
Normal file
236
api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
38
api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataFile.cs
Normal file
38
api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataFile.cs
Normal 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
|
||||
};
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)");
|
||||
|
||||
|
|
@ -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");
|
||||
|
||||
|
|
@ -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)");
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
13
claude.md
13
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
<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)}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
{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}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue