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)
|
├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise)
|
||||||
├── crud/{EntityName}.json ← tablonun REST uçları
|
├── crud/{EntityName}.json ← tablonun REST uçları
|
||||||
├── custom/{ComponentName}.json ← Custom Component (Visual Designer dokümanı ya da kod)
|
├── 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.
|
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 |
|
| 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 |
|
| 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 |
|
| 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`);
|
**`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
|
`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. Uçtan uca reçeteler
|
||||||
|
|
||||||
### 10.1 "Var olan bir tablo için ekran aç"
|
### 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/**/data/**
|
||||||
|
!configs/seeds/**/data/**
|
||||||
*.bak
|
*.bak
|
||||||
**/node_modules
|
**/node_modules
|
||||||
**/.DS_Store
|
**/.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
|
`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
|
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/
|
configs/seeds/
|
||||||
host/
|
host/
|
||||||
crud/ custom/ wizard/
|
crud/ custom/ wizard/ data/
|
||||||
sql/ { object/ execute/ }
|
sql/ { object/ execute/ }
|
||||||
postgres/ { object/ execute/ }
|
postgres/ { object/ execute/ }
|
||||||
tenants/
|
tenants/
|
||||||
{tenantId}/
|
{tenantId}/
|
||||||
crud/ custom/ wizard/
|
crud/ custom/ wizard/ data/
|
||||||
sql/ { object/ execute/ }
|
sql/ { object/ execute/ }
|
||||||
postgres/ { 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. |
|
| `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. |
|
| `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. |
|
| `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,
|
Klasör kökünde kalmış eski `.sql` dosyaları geriye dönük uyumluluk için hâlâ işlenir,
|
||||||
fakat `SqlDataSeeder` bunları uyarı ile loglar — ilgili klasöre taşınmaları beklenir.
|
fakat `SqlDataSeeder` bunları uyarı ile loglar — ilgili klasöre taşınmaları beklenir.
|
||||||
|
|
@ -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ı. |
|
| `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/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. |
|
| `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). |
|
| `SubFormsJson` | Ana-detay ilişkisi (ParentFieldName → ChildFieldName eşlemesi). |
|
||||||
| `WidgetsJson` | Ekranın üstünde gösterilecek KPI kartları. |
|
| `WidgetsJson` | Ekranın üstünde gösterilecek KPI kartları. |
|
||||||
| `WorkflowJson` | İş akışı bağlaması. |
|
| `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ı. |
|
| 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. |
|
| Bildirimler | `/admin/profile/notification-settings`, bildirim ekranları | Kanal tercihleri ve bildirim oluşturma. |
|
||||||
| Forum | `/admin/forum`, `/admin/forumManagement` | Forum kullanımı ve yönetimi. |
|
| 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ı. |
|
| 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ı. |
|
| 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ü. |
|
| AI asistanı | `/admin/ai` | n8n webhook üzerinden sohbet arayüzü. |
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,19 @@ public class GridOptionsEditDto : GridOptionsDto
|
||||||
set { SelectFieldsDefaultValueJson = JsonSerializer.Serialize(value); }
|
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 DeleteCommand { get; set; }
|
||||||
public string DeleteBeforeCommand { get; set; }
|
public string DeleteBeforeCommand { get; set; }
|
||||||
public string DeleteAfterCommand { get; set; }
|
public string DeleteAfterCommand { get; set; }
|
||||||
|
|
@ -170,6 +183,10 @@ public class GridOptionsEditDto : GridOptionsDto
|
||||||
target.DeleteCommand = DeleteCommand;
|
target.DeleteCommand = DeleteCommand;
|
||||||
target.DeleteBeforeCommand = DeleteBeforeCommand;
|
target.DeleteBeforeCommand = DeleteBeforeCommand;
|
||||||
target.DeleteAfterCommand = DeleteAfterCommand;
|
target.DeleteAfterCommand = DeleteAfterCommand;
|
||||||
|
target.SeedFilePath = SeedFilePath;
|
||||||
|
target.SeedSyncInsert = SeedSyncInsert;
|
||||||
|
target.SeedSyncUpdate = SeedSyncUpdate;
|
||||||
|
target.SeedSyncDelete = SeedSyncDelete;
|
||||||
|
|
||||||
target.SelectFieldsDefaultValueJson = SelectFieldsDefaultValueJson;
|
target.SelectFieldsDefaultValueJson = SelectFieldsDefaultValueJson;
|
||||||
target.InsertFieldsDefaultValueJson = InsertFieldsDefaultValueJson;
|
target.InsertFieldsDefaultValueJson = InsertFieldsDefaultValueJson;
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,10 @@ public class ListFormsAppService : CrudAppService<
|
||||||
item.IsTenant = input.IsTenant;
|
item.IsTenant = input.IsTenant;
|
||||||
item.IsBranch = input.IsBranch;
|
item.IsBranch = input.IsBranch;
|
||||||
item.IsOrganizationUnit = input.IsOrganizationUnit;
|
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)
|
else if (input.EditType == ListFormEditTabs.Database.Select.SelectForm)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,20 @@ public class ListFormDataAppService : PlatformAppService
|
||||||
private readonly IQueryManager qManager;
|
private readonly IQueryManager qManager;
|
||||||
private readonly IHttpContextAccessor httpContextAccessor;
|
private readonly IHttpContextAccessor httpContextAccessor;
|
||||||
private readonly IListFormSelectAppService listFormSelectAppService;
|
private readonly IListFormSelectAppService listFormSelectAppService;
|
||||||
|
private readonly IListFormSeedDataSynchronizer seedDataSynchronizer;
|
||||||
|
|
||||||
public ListFormDataAppService(
|
public ListFormDataAppService(
|
||||||
IListFormAuthorizationManager authManager,
|
IListFormAuthorizationManager authManager,
|
||||||
IQueryManager qManager,
|
IQueryManager qManager,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
IListFormSelectAppService listFormSelectAppService)
|
IListFormSelectAppService listFormSelectAppService,
|
||||||
|
IListFormSeedDataSynchronizer seedDataSynchronizer)
|
||||||
{
|
{
|
||||||
this.authManager = authManager;
|
this.authManager = authManager;
|
||||||
this.qManager = qManager;
|
this.qManager = qManager;
|
||||||
this.httpContextAccessor = httpContextAccessor;
|
this.httpContextAccessor = httpContextAccessor;
|
||||||
this.listFormSelectAppService = listFormSelectAppService;
|
this.listFormSelectAppService = listFormSelectAppService;
|
||||||
|
this.seedDataSynchronizer = seedDataSynchronizer;
|
||||||
LocalizationResource = typeof(PlatformResource);
|
LocalizationResource = typeof(PlatformResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +44,11 @@ public class ListFormDataAppService : PlatformAppService
|
||||||
?? throw new InvalidOperationException("HTTP Context bulunamadı.");
|
?? throw new InvalidOperationException("HTTP Context bulunamadı.");
|
||||||
|
|
||||||
var queryParameters = httpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value);
|
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)
|
public async Task<dynamic> PostDuplicateAsync(DataRequestDto input)
|
||||||
|
|
@ -74,7 +81,11 @@ public class ListFormDataAppService : PlatformAppService
|
||||||
|
|
||||||
input.Data = JsonSerializer.Serialize(dict);
|
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)
|
public async Task<int> PostUpdateAsync(DataRequestDto input)
|
||||||
|
|
@ -87,7 +98,11 @@ public class ListFormDataAppService : PlatformAppService
|
||||||
?? throw new InvalidOperationException("HTTP Context bulunamadı.");
|
?? throw new InvalidOperationException("HTTP Context bulunamadı.");
|
||||||
|
|
||||||
var queryParameters = httpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value);
|
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)
|
public async Task<int> PostDeleteAsync(DataRequestDto input)
|
||||||
|
|
@ -100,7 +115,11 @@ public class ListFormDataAppService : PlatformAppService
|
||||||
?? throw new InvalidOperationException("HTTP Context bulunamadı.");
|
?? throw new InvalidOperationException("HTTP Context bulunamadı.");
|
||||||
|
|
||||||
var queryParameters = httpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value);
|
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 IConfiguration configuration;
|
||||||
private readonly IOptions<IdentityOptions> identityOptions;
|
private readonly IOptions<IdentityOptions> identityOptions;
|
||||||
private readonly IRepository<Order, Guid> orderRepository;
|
private readonly IRepository<Order, Guid> orderRepository;
|
||||||
|
private readonly IListFormSeedDataSynchronizer seedDataSynchronizer;
|
||||||
|
|
||||||
public ListFormDynamicApiAppService(
|
public ListFormDynamicApiAppService(
|
||||||
ITenantRepository tenantRepository,
|
ITenantRepository tenantRepository,
|
||||||
|
|
@ -44,7 +45,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
|
||||||
BlobManager blobCdnManager,
|
BlobManager blobCdnManager,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IOptions<IdentityOptions> identityOptions,
|
IOptions<IdentityOptions> identityOptions,
|
||||||
IRepository<Order, Guid> orderRepository)
|
IRepository<Order, Guid> orderRepository,
|
||||||
|
IListFormSeedDataSynchronizer seedDataSynchronizer)
|
||||||
{
|
{
|
||||||
this.tenantRepository = tenantRepository;
|
this.tenantRepository = tenantRepository;
|
||||||
this.tenantManager = tenantManager;
|
this.tenantManager = tenantManager;
|
||||||
|
|
@ -54,6 +56,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
this.identityOptions = identityOptions;
|
this.identityOptions = identityOptions;
|
||||||
this.orderRepository = orderRepository;
|
this.orderRepository = orderRepository;
|
||||||
|
this.seedDataSynchronizer = seedDataSynchronizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Guid ParseGuid(string value)
|
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.CreateAsync(user, input.Data.Password)).CheckErrors();
|
||||||
await userManager.SetLockoutEnabledAsync(user, true);
|
await userManager.SetLockoutEnabledAsync(user, true);
|
||||||
|
|
||||||
|
await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, user.Id, SeedSyncOperation.Insert);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(IdentityPermissions.Users.Update)]
|
[Authorize(IdentityPermissions.Users.Update)]
|
||||||
|
|
@ -198,18 +203,22 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
|
||||||
await SaveAvatarAsync(user, input.Data.Avatar);
|
await SaveAvatarAsync(user, input.Data.Avatar);
|
||||||
|
|
||||||
(await userManager.UpdateAsync(user)).CheckErrors();
|
(await userManager.UpdateAsync(user)).CheckErrors();
|
||||||
|
|
||||||
|
await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, user.Id, SeedSyncOperation.Update);
|
||||||
}
|
}
|
||||||
|
|
||||||
//RoleAppService
|
//RoleAppService
|
||||||
[Authorize(IdentityPermissions.Roles.Create)]
|
[Authorize(IdentityPermissions.Roles.Create)]
|
||||||
public async Task PostRoleInsertAsync(DynamicApiBaseInput<CreateUpdateRoleInput> input)
|
public async Task PostRoleInsertAsync(DynamicApiBaseInput<CreateUpdateRoleInput> input)
|
||||||
{
|
{
|
||||||
await identityRoleAppService.CreateAsync(new IdentityRoleCreateDto
|
var role = await identityRoleAppService.CreateAsync(new IdentityRoleCreateDto
|
||||||
{
|
{
|
||||||
Name = input.Data.Name,
|
Name = input.Data.Name,
|
||||||
IsDefault = input.Data.IsDefault ?? false,
|
IsDefault = input.Data.IsDefault ?? false,
|
||||||
IsPublic = input.Data.IsPublic ?? false,
|
IsPublic = input.Data.IsPublic ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, role.Id, SeedSyncOperation.Insert);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(IdentityPermissions.Roles.Update)]
|
[Authorize(IdentityPermissions.Roles.Update)]
|
||||||
|
|
@ -229,12 +238,16 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
|
||||||
};
|
};
|
||||||
|
|
||||||
await identityRoleAppService.UpdateAsync(id, role);
|
await identityRoleAppService.UpdateAsync(id, role);
|
||||||
|
|
||||||
|
await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, id, SeedSyncOperation.Update);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(TenantManagementPermissions.Tenants.Create)]
|
[Authorize(TenantManagementPermissions.Tenants.Create)]
|
||||||
public async Task PostTenantInsertAsync(DynamicApiBaseInput<CreateUpdateTenantInput> input)
|
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)]
|
[Authorize(TenantManagementPermissions.Tenants.Create)]
|
||||||
|
|
@ -353,6 +366,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
|
||||||
entity.SetModules(input.Data.Modules);
|
entity.SetModules(input.Data.Modules);
|
||||||
|
|
||||||
await tenantRepository.UpdateAsync(entity, autoSave: true);
|
await tenantRepository.UpdateAsync(entity, autoSave: true);
|
||||||
|
|
||||||
|
await seedDataSynchronizer.SyncRowAsync(input.ListFormCode, id, SeedSyncOperation.Update);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(TenantManagementPermissions.Tenants.Delete)]
|
[Authorize(TenantManagementPermissions.Tenants.Delete)]
|
||||||
|
|
@ -364,6 +379,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
|
||||||
}
|
}
|
||||||
var id = Guid.Parse(input.Keys[0]!.ToString()!);
|
var id = Guid.Parse(input.Keys[0]!.ToString()!);
|
||||||
await tenantRepository.DeleteAsync(id);
|
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 IListFormAuthorizationManager _authManager;
|
||||||
private readonly IQueryManager _qManager;
|
private readonly IQueryManager _qManager;
|
||||||
private readonly BlobManager _blobContainer;
|
private readonly BlobManager _blobContainer;
|
||||||
|
private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer;
|
||||||
|
|
||||||
public ListFormImportAppService(
|
public ListFormImportAppService(
|
||||||
IRepository<ListFormImport, Guid> importSessionRepository,
|
IRepository<ListFormImport, Guid> importSessionRepository,
|
||||||
IRepository<ListFormImportLog, Guid> importSessionExecuteRepository,
|
IRepository<ListFormImportLog, Guid> importSessionExecuteRepository,
|
||||||
IListFormAuthorizationManager authManager,
|
IListFormAuthorizationManager authManager,
|
||||||
IQueryManager qManager,
|
IQueryManager qManager,
|
||||||
BlobManager blobContainer
|
BlobManager blobContainer,
|
||||||
|
IListFormSeedDataSynchronizer seedDataSynchronizer
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_importSessionRepository = importSessionRepository;
|
_importSessionRepository = importSessionRepository;
|
||||||
|
|
@ -42,6 +44,7 @@ public class ListFormImportAppService : PlatformAppService, IImportAppService
|
||||||
_blobContainer = blobContainer;
|
_blobContainer = blobContainer;
|
||||||
_authManager = authManager;
|
_authManager = authManager;
|
||||||
_qManager = qManager;
|
_qManager = qManager;
|
||||||
|
_seedDataSynchronizer = seedDataSynchronizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ListFormsImportDto> CreateAsync(
|
public async Task<ListFormsImportDto> CreateAsync(
|
||||||
|
|
@ -224,6 +227,10 @@ public class ListFormImportAppService : PlatformAppService, IImportAppService
|
||||||
session.Status = errorCount > 0 ? "executed_with_errors" : "executed";
|
session.Status = errorCount > 0 ? "executed_with_errors" : "executed";
|
||||||
await _importSessionRepository.UpdateAsync(session, autoSave: true);
|
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);
|
return ObjectMapper.Map<ListFormImportLog, ListFormImportLogDto>(execute);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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",
|
"en": "Control Type",
|
||||||
"tr": "Kontrol Türü"
|
"tr": "Kontrol Türü"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.ListFormEdit.ScopeOptions",
|
||||||
|
"en": "Scope",
|
||||||
|
"tr": "Kapsam"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.ListFormEdit.IsTenant",
|
"key": "App.ListFormEdit.IsTenant",
|
||||||
|
|
@ -4752,6 +4758,42 @@
|
||||||
"en": "Assignments to be made to the fields when Save is pressed while updating the record",
|
"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"
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.ListFormEdit.DatabaseDeleteServiceAddress",
|
"key": "App.ListFormEdit.DatabaseDeleteServiceAddress",
|
||||||
|
|
|
||||||
|
|
@ -10133,6 +10133,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
ColumnOptionJson = DefaultColumnOptionJson(),
|
ColumnOptionJson = DefaultColumnOptionJson(),
|
||||||
PermissionJson = DefaultPermissionJson(listFormName),
|
PermissionJson = DefaultPermissionJson(listFormName),
|
||||||
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.IntranetWidget)),
|
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.IntranetWidget)),
|
||||||
|
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
||||||
|
SeedSyncInsert = true,
|
||||||
|
SeedSyncUpdate = true,
|
||||||
|
SeedSyncDelete = true,
|
||||||
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||||
PagerOptionJson = DefaultPagerOptionJson,
|
PagerOptionJson = DefaultPagerOptionJson,
|
||||||
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
|
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,18 @@ public interface IListFormArtifact
|
||||||
string DeleteBeforeCommand { get; set; }
|
string DeleteBeforeCommand { get; set; }
|
||||||
string DeleteAfterCommand { 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 SelectFieldsDefaultValueJson { get; set; }
|
||||||
string InsertFieldsDefaultValueJson { get; set; }
|
string InsertFieldsDefaultValueJson { get; set; }
|
||||||
string UpdateFieldsDefaultValueJson { get; set; }
|
string UpdateFieldsDefaultValueJson { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ public static class TableNameResolver
|
||||||
{ nameof(TableNameEnum.Agenda), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
{ nameof(TableNameEnum.Agenda), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||||
|
|
||||||
// 🔹 INTRANET TABLOLARI
|
// 🔹 INTRANET TABLOLARI
|
||||||
|
{ nameof(TableNameEnum.IntranetWidget), (TablePrefix.TenantByName, MenuPrefix.Sas) },
|
||||||
{ nameof(TableNameEnum.Announcement), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
{ nameof(TableNameEnum.Announcement), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||||
{ nameof(TableNameEnum.AnnouncementComment), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
{ nameof(TableNameEnum.AnnouncementComment), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||||
{ nameof(TableNameEnum.AnnouncementLike), (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.Event), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||||
{ nameof(TableNameEnum.EventPhoto), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
{ nameof(TableNameEnum.EventPhoto), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||||
{ nameof(TableNameEnum.EventComment), (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.MessengerConversation), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||||
{ nameof(TableNameEnum.MessengerConversationMessage), (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.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using Volo.Abp.Data;
|
using Volo.Abp.Data;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
@ -27,6 +28,7 @@ public class PlatformDbMigrationService : ITransientDependency
|
||||||
private readonly ICurrentTenant _currentTenant;
|
private readonly ICurrentTenant _currentTenant;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IEnumerable<IAfterAllMigrationsSqlExecutor> _afterAllMigrationsSqlExecutors;
|
private readonly IEnumerable<IAfterAllMigrationsSqlExecutor> _afterAllMigrationsSqlExecutors;
|
||||||
|
private readonly IListFormSeedDataApplier _listFormSeedDataApplier;
|
||||||
|
|
||||||
public PlatformDbMigrationService(
|
public PlatformDbMigrationService(
|
||||||
IDataSeeder dataSeeder,
|
IDataSeeder dataSeeder,
|
||||||
|
|
@ -34,7 +36,8 @@ public class PlatformDbMigrationService : ITransientDependency
|
||||||
ITenantRepository tenantRepository,
|
ITenantRepository tenantRepository,
|
||||||
ICurrentTenant currentTenant,
|
ICurrentTenant currentTenant,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IEnumerable<IAfterAllMigrationsSqlExecutor> afterAllMigrationsSqlExecutors)
|
IEnumerable<IAfterAllMigrationsSqlExecutor> afterAllMigrationsSqlExecutors,
|
||||||
|
IListFormSeedDataApplier listFormSeedDataApplier)
|
||||||
{
|
{
|
||||||
_dataSeeder = dataSeeder;
|
_dataSeeder = dataSeeder;
|
||||||
_dbSchemaMigrators = dbSchemaMigrators;
|
_dbSchemaMigrators = dbSchemaMigrators;
|
||||||
|
|
@ -42,6 +45,7 @@ public class PlatformDbMigrationService : ITransientDependency
|
||||||
_currentTenant = currentTenant;
|
_currentTenant = currentTenant;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_afterAllMigrationsSqlExecutors = afterAllMigrationsSqlExecutors;
|
_afterAllMigrationsSqlExecutors = afterAllMigrationsSqlExecutors;
|
||||||
|
_listFormSeedDataApplier = listFormSeedDataApplier;
|
||||||
|
|
||||||
Logger = NullLogger<PlatformDbMigrationService>.Instance;
|
Logger = NullLogger<PlatformDbMigrationService>.Instance;
|
||||||
}
|
}
|
||||||
|
|
@ -177,6 +181,10 @@ public class PlatformDbMigrationService : ITransientDependency
|
||||||
.WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, PlatformConsts.AbpIdentity.User.AdminPasswordDefaultValue)
|
.WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, PlatformConsts.AbpIdentity.User.AdminPasswordDefaultValue)
|
||||||
.WithProperty(IdentityDataSeedContributor.AdminUserNamePropertyName, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue)
|
.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()
|
private bool AddInitialMigrationIfNotExist()
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ namespace Sozsoft.Platform.Data.Seeds;
|
||||||
/// crud/
|
/// crud/
|
||||||
/// custom/
|
/// custom/
|
||||||
/// wizard/
|
/// wizard/
|
||||||
|
/// data/
|
||||||
/// sql/ → SQL Server saglayicisi
|
/// sql/ → SQL Server saglayicisi
|
||||||
/// object/ → nesne olusturulur/guncellenir
|
/// object/ → nesne olusturulur/guncellenir
|
||||||
/// execute/ → nesne olusturulur, ayrica tum migration'lar bitince calistirilir
|
/// execute/ → nesne olusturulur, ayrica tum migration'lar bitince calistirilir
|
||||||
|
|
@ -33,7 +34,7 @@ namespace Sozsoft.Platform.Data.Seeds;
|
||||||
/// execute/
|
/// execute/
|
||||||
/// tenants/
|
/// tenants/
|
||||||
/// {tenantId}/ → her tenant kendi klasorunde
|
/// {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>
|
/// </code>
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -74,6 +75,15 @@ public static class SeedPathResolver
|
||||||
/// <summary>Custom component konfigurasyon dosyalari.</summary>
|
/// <summary>Custom component konfigurasyon dosyalari.</summary>
|
||||||
public const string CustomFolder = "custom";
|
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 RepositorySeedsFolder = "seeds";
|
||||||
private const string RepositoryConfigsFolder = "configs";
|
private const string RepositoryConfigsFolder = "configs";
|
||||||
private const string FallbackFolderName = "Seeds";
|
private const string FallbackFolderName = "Seeds";
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,22 @@ public class ListForm : Entity<Guid>, IListFormArtifact
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DeleteServiceAddress { get; set; } = "list-form-data/delete";
|
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 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
|
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.UpdateServiceAddress).HasMaxLength(256);
|
||||||
b.Property(a => a.InsertServiceAddress).HasMaxLength(256);
|
b.Property(a => a.InsertServiceAddress).HasMaxLength(256);
|
||||||
b.Property(a => a.DeleteServiceAddress).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.UserId).HasMaxLength(256);
|
||||||
b.Property(a => a.RoleId).HasMaxLength(256);
|
b.Property(a => a.RoleId).HasMaxLength(256);
|
||||||
b.Property(a => a.ListFormType).HasMaxLength(20);
|
b.Property(a => a.ListFormType).HasMaxLength(20);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
||||||
namespace Sozsoft.Platform.Migrations
|
namespace Sozsoft.Platform.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PlatformDbContext))]
|
[DbContext(typeof(PlatformDbContext))]
|
||||||
[Migration("20260903062758_Initial")]
|
[Migration("20260904084415_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -3189,7 +3189,7 @@ namespace Sozsoft.Platform.Migrations
|
||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasFilter("[IsDeleted] = 0");
|
.HasFilter("[IsDeleted] = 0");
|
||||||
|
|
||||||
b.ToTable("Adm_T_IntranetWidget", (string)null);
|
b.ToTable("Sas_T_IntranetWidget", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Sozsoft.Platform.Entities.IpRestriction", b =>
|
modelBuilder.Entity("Sozsoft.Platform.Entities.IpRestriction", b =>
|
||||||
|
|
@ -3526,6 +3526,25 @@ namespace Sozsoft.Platform.Migrations
|
||||||
b.Property<string>("SearchPanelJson")
|
b.Property<string>("SearchPanelJson")
|
||||||
.HasColumnType("nvarchar(max)");
|
.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")
|
b.Property<string>("SelectCommand")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
|
@ -601,35 +601,6 @@ namespace Sozsoft.Platform.Migrations
|
||||||
table.PrimaryKey("PK_Adm_T_EventType", x => x.Id);
|
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(
|
migrationBuilder.CreateTable(
|
||||||
name: "Adm_T_IpRestriction",
|
name: "Adm_T_IpRestriction",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
|
|
@ -1531,6 +1502,10 @@ namespace Sozsoft.Platform.Migrations
|
||||||
UpdateServiceAddress = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
UpdateServiceAddress = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||||
InsertServiceAddress = 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),
|
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),
|
CustomJsSourcesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
CustomStyleSourcesJson = 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),
|
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);
|
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(
|
migrationBuilder.CreateTable(
|
||||||
name: "AbpAuditLogActions",
|
name: "AbpAuditLogActions",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
|
|
@ -3867,13 +3871,6 @@ namespace Sozsoft.Platform.Migrations
|
||||||
unique: true,
|
unique: true,
|
||||||
filter: "[IsDeleted] = 0");
|
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(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_Adm_T_IpRestriction_TenantId_ResourceType_ResourceId_IP",
|
name: "IX_Adm_T_IpRestriction_TenantId_ResourceType_ResourceId_IP",
|
||||||
table: "Adm_T_IpRestriction",
|
table: "Adm_T_IpRestriction",
|
||||||
|
|
@ -4406,6 +4403,13 @@ namespace Sozsoft.Platform.Migrations
|
||||||
name: "IX_Sas_T_ForumTopic_CategoryId",
|
name: "IX_Sas_T_ForumTopic_CategoryId",
|
||||||
table: "Sas_T_ForumTopic",
|
table: "Sas_T_ForumTopic",
|
||||||
column: "CategoryId");
|
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 />
|
/// <inheritdoc />
|
||||||
|
|
@ -4501,9 +4505,6 @@ namespace Sozsoft.Platform.Migrations
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Adm_T_EventLike");
|
name: "Adm_T_EventLike");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Adm_T_IntranetWidget");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Adm_T_IpRestriction");
|
name: "Adm_T_IpRestriction");
|
||||||
|
|
||||||
|
|
@ -4684,6 +4685,9 @@ namespace Sozsoft.Platform.Migrations
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Sas_T_GlobalSearch");
|
name: "Sas_T_GlobalSearch");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Sas_T_IntranetWidget");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "AbpEntityChanges");
|
name: "AbpEntityChanges");
|
||||||
|
|
||||||
|
|
@ -3186,7 +3186,7 @@ namespace Sozsoft.Platform.Migrations
|
||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasFilter("[IsDeleted] = 0");
|
.HasFilter("[IsDeleted] = 0");
|
||||||
|
|
||||||
b.ToTable("Adm_T_IntranetWidget", (string)null);
|
b.ToTable("Sas_T_IntranetWidget", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Sozsoft.Platform.Entities.IpRestriction", b =>
|
modelBuilder.Entity("Sozsoft.Platform.Entities.IpRestriction", b =>
|
||||||
|
|
@ -3523,6 +3523,25 @@ namespace Sozsoft.Platform.Migrations
|
||||||
b.Property<string>("SearchPanelJson")
|
b.Property<string>("SearchPanelJson")
|
||||||
.HasColumnType("nvarchar(max)");
|
.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")
|
b.Property<string>("SelectCommand")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2239,91 +2239,5 @@
|
||||||
"FormatTemplate": "{Prefix}-{yyyy}{MM}{dd}-{Number}",
|
"FormatTemplate": "{Prefix}-{yyyy}{MM}{dd}-{Number}",
|
||||||
"ResetPeriod": "Daily"
|
"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<EventSeedDto> Events { get; set; }
|
||||||
public List<EventPhotoSeedDto> EventPhotos { get; set; }
|
public List<EventPhotoSeedDto> EventPhotos { get; set; }
|
||||||
public List<EventCommentSeedDto> EventComments { get; set; }
|
public List<EventCommentSeedDto> EventComments { get; set; }
|
||||||
public List<IntranetWidgetSeedDto> Widgets { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SequenceSeedDto
|
public class SequenceSeedDto
|
||||||
|
|
@ -196,20 +195,6 @@ public class SurveyQuestionOptionSeedDto
|
||||||
public int Order { get; set; }
|
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 class AnnouncementSeedDto
|
||||||
{
|
{
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
@ -502,7 +487,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
private readonly IRepository<Department, Guid> _departmentRepository;
|
private readonly IRepository<Department, Guid> _departmentRepository;
|
||||||
private readonly IRepository<JobPosition, Guid> _jobPositionRepository;
|
private readonly IRepository<JobPosition, Guid> _jobPositionRepository;
|
||||||
private readonly IRepository<Announcement, Guid> _announcementRepository;
|
private readonly IRepository<Announcement, Guid> _announcementRepository;
|
||||||
private readonly IRepository<IntranetWidget, Guid> _intranetWidgetRepository;
|
|
||||||
private readonly IRepository<Survey, Guid> _surveyRepository;
|
private readonly IRepository<Survey, Guid> _surveyRepository;
|
||||||
private readonly IRepository<SurveyQuestion, Guid> _surveyQuestionRepository;
|
private readonly IRepository<SurveyQuestion, Guid> _surveyQuestionRepository;
|
||||||
private readonly IRepository<SurveyQuestionOption, Guid> _surveyQuestionOptionRepository;
|
private readonly IRepository<SurveyQuestionOption, Guid> _surveyQuestionOptionRepository;
|
||||||
|
|
@ -547,7 +531,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
IRepository<JobPosition, Guid> jobPositionRepository,
|
IRepository<JobPosition, Guid> jobPositionRepository,
|
||||||
|
|
||||||
IRepository<Announcement, Guid> announcementRepository,
|
IRepository<Announcement, Guid> announcementRepository,
|
||||||
IRepository<IntranetWidget, Guid> intranetWidgetRepository,
|
|
||||||
IRepository<Survey, Guid> surveyRepository,
|
IRepository<Survey, Guid> surveyRepository,
|
||||||
IRepository<SurveyQuestion, Guid> surveyQuestionRepository,
|
IRepository<SurveyQuestion, Guid> surveyQuestionRepository,
|
||||||
IRepository<SurveyQuestionOption, Guid> surveyQuestionOptionRepository,
|
IRepository<SurveyQuestionOption, Guid> surveyQuestionOptionRepository,
|
||||||
|
|
@ -589,7 +572,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
_departmentRepository = departmentRepository;
|
_departmentRepository = departmentRepository;
|
||||||
_jobPositionRepository = jobPositionRepository;
|
_jobPositionRepository = jobPositionRepository;
|
||||||
_announcementRepository = announcementRepository;
|
_announcementRepository = announcementRepository;
|
||||||
_intranetWidgetRepository = intranetWidgetRepository;
|
|
||||||
_surveyRepository = surveyRepository;
|
_surveyRepository = surveyRepository;
|
||||||
_surveyQuestionRepository = surveyQuestionRepository;
|
_surveyQuestionRepository = surveyQuestionRepository;
|
||||||
_surveyQuestionOptionRepository = surveyQuestionOptionRepository;
|
_surveyQuestionOptionRepository = surveyQuestionOptionRepository;
|
||||||
|
|
@ -1024,26 +1006,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
}, autoSave: true);
|
}, 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)
|
foreach (var item in items.Announcements)
|
||||||
{
|
{
|
||||||
var exists = await _announcementRepository.AnyAsync(x => x.Title == item.Title);
|
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`.
|
`crud/{Entity}.json`.
|
||||||
- **Wizard dosyası** → tek başına taşınmaz; export zip'i `wizard/`, `custom/`, `crud/` ve
|
- **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.
|
`{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`.
|
- **`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.
|
- **Ç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;
|
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.
|
`IDE00xx` yok.
|
||||||
- `ui/` değişikliğinden sonra: `npm run typecheck` ve `npm run lint`.
|
- `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.
|
- 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
|
### 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
|
updateServiceAddress?: string
|
||||||
insertServiceAddress?: string
|
insertServiceAddress?: string
|
||||||
deleteServiceAddress?: 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
|
isTenant: boolean
|
||||||
isBranch: boolean
|
isBranch: boolean
|
||||||
isOrganizationUnit: boolean
|
isOrganizationUnit: boolean
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,16 @@ const schema = Yup.object().shape({
|
||||||
tableName: Yup.string(),
|
tableName: Yup.string(),
|
||||||
keyFieldName: Yup.string(),
|
keyFieldName: Yup.string(),
|
||||||
keyFieldDbSourceType: 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) {
|
function FormTabDatabaseDataSource(props: FormEditProps) {
|
||||||
const initialValues = useStoreState((s) => s.admin.lists.values)
|
const initialValues = useStoreState((s) => s.admin.lists.values)
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
@ -143,41 +151,21 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
|
||||||
<Form>
|
<Form>
|
||||||
<FormContainer size="sm">
|
<FormContainer size="sm">
|
||||||
<Card className="mt-2 mb-2">
|
<Card className="mt-2 mb-2">
|
||||||
<FormItem
|
<FormItem label={translate('::App.ListFormEdit.ScopeOptions')}>
|
||||||
label={translate('::App.ListFormEdit.IsTenant')}
|
<div className="flex flex-wrap gap-6">
|
||||||
invalid={errors.editingOptionDto?.isTenant && touched.editingOptionDto?.isTenant}
|
{scopeFields.map((name) => (
|
||||||
errorMessage={errors.editingOptionDto?.isTenant}
|
<Field key={name} name={name}>
|
||||||
>
|
{({ field, form }: FieldProps<boolean>) => (
|
||||||
<Field
|
<Checkbox
|
||||||
name="isTenant"
|
checked={field.value === true}
|
||||||
placeholder={translate('::App.ListFormEdit.IsTenant')}
|
onChange={(checked) => form.setFieldValue(field.name, checked)}
|
||||||
component={Checkbox}
|
>
|
||||||
/>
|
{translate(`::App.ListFormEdit.${name[0].toUpperCase()}${name.slice(1)}`)}
|
||||||
</FormItem>
|
</Checkbox>
|
||||||
<FormItem
|
)}
|
||||||
label={translate('::App.ListFormEdit.IsBranch')}
|
</Field>
|
||||||
invalid={errors.editingOptionDto?.isBranch && touched.editingOptionDto?.isBranch}
|
))}
|
||||||
errorMessage={errors.editingOptionDto?.isBranch}
|
</div>
|
||||||
>
|
|
||||||
<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>
|
</FormItem>
|
||||||
<FormItem
|
<FormItem
|
||||||
label={translate('::App.Listform.ListformField.DataSourceCode')}
|
label={translate('::App.Listform.ListformField.DataSourceCode')}
|
||||||
|
|
@ -403,6 +391,40 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</FormItem>
|
</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>
|
</Card>
|
||||||
|
|
||||||
<Button block size="sm" variant="solid" loading={isSubmitting}>
|
<Button block size="sm" variant="solid" loading={isSubmitting}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue