MenuManager komponenti json dosyası güncellenebiliyor.

This commit is contained in:
Sedat ÖZTÜRK 2026-10-01 12:37:30 +03:00
parent 61706220b1
commit c4e90a459f
7 changed files with 690 additions and 214 deletions

View file

@ -3995,8 +3995,9 @@ elle düzeltme ve wizard'ın kapsamadığı düzenlemeler içindir.
kullanır; custom component'lerin route'u `RoutePath` alanından türetilir. `Route` tablosu kullanır; custom component'lerin route'u `RoutePath` alanından türetilir. `Route` tablosu
yalnızca fiziksel React sayfaları içindir ve prompt ile ekran üretirken böyle bir sayfa yalnızca fiziksel React sayfaları içindir ve prompt ile ekran üretirken böyle bir sayfa
yazılmaz. yazılmaz.
3. **Menü sırası** için wizard'ın `MenuOrder` alanını kullan; Menu Manager'da elle taşımak seed 3. **Menü sırası** için wizard'ın `MenuOrder` alanını kullan. Menu Manager'daki taşıma/düzenleme de
dosyasına yansımaz ve veritabanı sıfırlandığında kaybolur. `App.Menus.Menu` ekranının seed dosyasına (`data/App.Menus.Menu.json`) ve menü adı
`App.Languages.LanguageText.json`'a yansır (`MenuAppService` → `IListFormSeedDataSynchronizer`).
4. **Rol/dil bazlı menü varyantı** gerekiyorsa Menu List ekranından ikinci bir kayıt açılır — 4. **Rol/dil bazlı menü varyantı** gerekiyorsa Menu List ekranından ikinci bir kayıt açılır —
aynı `Url`, farklı `RoleId`/`CultureName`. Wizard bunu üretmez. aynı `Url`, farklı `RoleId`/`CultureName`. Wizard bunu üretmez.

View file

@ -899,7 +899,7 @@ Route.Authority ──┘ └► Permission
- **Menu** (`Menu.cs`): `Code`, `ParentCode`, `DisplayName`, `Url`, `Icon`, `Order`, - **Menu** (`Menu.cs`): `Code`, `ParentCode`, `DisplayName`, `Url`, `Icon`, `Order`,
`RequiredPermissionName`, `Target`, `IsDisabled`, ayrıca `UserId` / `RoleId` / `CultureName` `RequiredPermissionName`, `Target`, `IsDisabled`, ayrıca `UserId` / `RoleId` / `CultureName`
ile kullanıcı, rol ve dil bazlı menü varyantları. ile kullanıcı, rol ve dil bazlı menü varyantları.
- **Menü yöneticisi** (`/admin/menuManager`): sürükle-bırak ağaç ile menü düzenleme. - **Menü yöneticisi** (`/admin/menuManager`): sürükle-bırak ağaç ile menü düzenleme. Kayıtlar `App.Menus.Menu` ekranıyla aynı seed dosyasına (`data/App.Menus.Menu.json`, `SeedSync*` bayraklarına bağlı) yazılır; menü adları `App.Languages.LanguageText.json`'a.
- Frontend'de `dynamicRoutesContext` route'ları çeker, `dynamicRouteLoader` bunları fiziksel - Frontend'de `dynamicRoutesContext` route'ları çeker, `dynamicRouteLoader` bunları fiziksel
view'lara veya runtime derlenmiş Custom Component'lere eşler, `DynamicRouter` React Router view'lara veya runtime derlenmiş Custom Component'lere eşler, `DynamicRouter` React Router
ağacını kurar; her korumalı route `ProtectedRoute` + `PermissionGuard` ile sarılır. ağacını kurar; her korumalı route `ProtectedRoute` + `PermissionGuard` ile sarılır.

View file

@ -13,6 +13,7 @@ using Sozsoft.Platform.ListForms.Select;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
namespace Sozsoft.Platform.ListForms; namespace Sozsoft.Platform.ListForms;
@ -28,6 +29,12 @@ public interface IListFormSeedDataSynchronizer
/// </summary> /// </summary>
Task SyncRowAsync(string listFormCode, object key, SeedSyncOperation operation); Task SyncRowAsync(string listFormCode, object key, SeedSyncOperation operation);
/// <summary>
/// <see cref="SyncRowAsync"/>'in toplu hali: tek dosya modunda dosya bir kez okunup bir kez yazilir.
/// Siralama gibi bircok satiri birlikte degistiren islemler icindir.
/// </summary>
Task SyncRowsAsync(string listFormCode, IReadOnlyCollection<object> keys, SeedSyncOperation operation);
/// <summary>Silinen satirlari dosyadan cikarir; <c>SeedSyncDelete</c> kapaliysa hicbir sey yapmaz.</summary> /// <summary>Silinen satirlari dosyadan cikarir; <c>SeedSyncDelete</c> kapaliysa hicbir sey yapmaz.</summary>
Task RemoveRowsAsync(string listFormCode, object[] keys); Task RemoveRowsAsync(string listFormCode, object[] keys);
@ -70,6 +77,7 @@ public class ListFormSeedDataSynchronizer(
IListFormSelectAppService listFormSelectAppService, IListFormSelectAppService listFormSelectAppService,
IConfiguration configuration, IConfiguration configuration,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
ILogger<ListFormSeedDataSynchronizer> logger) : IListFormSeedDataSynchronizer, ITransientDependency ILogger<ListFormSeedDataSynchronizer> logger) : IListFormSeedDataSynchronizer, ITransientDependency
{ {
private const int SyncAllPageSize = 10000; private const int SyncAllPageSize = 10000;
@ -80,12 +88,18 @@ public class ListFormSeedDataSynchronizer(
/// </summary> /// </summary>
private sealed record SeedTarget(string Path, bool IsFolder); private sealed record SeedTarget(string Path, bool IsFolder);
public async Task SyncRowAsync(string listFormCode, object key, SeedSyncOperation operation) public Task SyncRowAsync(string listFormCode, object key, SeedSyncOperation operation) =>
SyncRowsAsync(listFormCode, [key], operation);
public async Task SyncRowsAsync(string listFormCode, IReadOnlyCollection<object> keys, SeedSyncOperation operation)
{ {
await ExecuteAsync(listFormCode, IsEnabled(operation), async (listForm, target) => await ExecuteAsync(listFormCode, IsEnabled(operation), async (listForm, target) =>
{ {
// Insert ucu her veri kaynaginda anahtari geri dondurmez; anahtarsiz kaldigimizda // Insert ucu her veri kaynaginda anahtari geri dondurmez; anahtarsiz kaldigimizda
// ya da satir okunamadiginda hedef listenin tamamiyla yenilenir, bayat kalmaz. // ya da satir okunamadiginda hedef listenin tamamiyla yenilenir, bayat kalmaz.
var rows = new List<(object Key, Dictionary<string, object> Row)>();
foreach (var key in keys ?? [])
{
var row = key == null ? null : await ReadRowAsync(listForm, key); var row = key == null ? null : await ReadRowAsync(listForm, key);
if (row == null) if (row == null)
{ {
@ -94,19 +108,31 @@ public class ListFormSeedDataSynchronizer(
return; return;
} }
rows.Add((key, row));
}
if (rows.Count == 0)
return;
if (target.IsFolder) if (target.IsFolder)
{ {
foreach (var (key, row) in rows)
await WriteRowFileAsync(listForm, target, key, row); await WriteRowFileAsync(listForm, target, key, row);
return; return;
} }
var seedFile = await ReadSeedFileAsync(target.Path, listForm); var seedFile = await ReadSeedFileAsync(target.Path, listForm);
foreach (var (key, row) in rows)
{
var index = FindRowIndex(seedFile, listForm.KeyFieldName, key); var index = FindRowIndex(seedFile, listForm.KeyFieldName, key);
if (index >= 0) if (index >= 0)
seedFile.Rows[index] = row; seedFile.Rows[index] = row;
else else
seedFile.Rows.Add(row); seedFile.Rows.Add(row);
}
await WriteSeedFileAsync(target.Path, seedFile); await WriteSeedFileAsync(target.Path, seedFile);
}); });
@ -171,7 +197,9 @@ public class ListFormSeedDataSynchronizer(
row.Any(kv => string.Equals(kv.Key, fieldName, StringComparison.OrdinalIgnoreCase) && row.Any(kv => string.Equals(kv.Key, fieldName, StringComparison.OrdinalIgnoreCase) &&
wanted.Contains(ToComparableText(kv.Value))); wanted.Contains(ToComparableText(kv.Value)));
await ExecuteAsync(listFormCode, listForm => listForm.SeedSyncDelete, async (_, target) => // Yalnizca dosyaya dokunur, veritabanindan satir okumaz; commit beklenmez. Ertelenseydi
// ayni istekte sonradan yazilan wizard satirlarini silebilirdi.
await RunAsync(listFormCode, listForm => listForm.SeedSyncDelete, async (_, target) =>
{ {
List<string> files = target.IsFolder List<string> files = target.IsFolder
? Directory.Exists(target.Path) ? [.. Directory.EnumerateFiles(target.Path, "*.json")] : [] ? Directory.Exists(target.Path) ? [.. Directory.EnumerateFiles(target.Path, "*.json")] : []
@ -345,15 +373,37 @@ public class ListFormSeedDataSynchronizer(
/// Seed dosyasi bagli degilse ya da islemin bayragi kapaliysa hicbir sey yapmaz. /// Seed dosyasi bagli degilse ya da islemin bayragi kapaliysa hicbir sey yapmaz.
/// Senkronizasyon hatasi kullanicinin kayit islemini bozmaz; yalnizca loglanir. /// Senkronizasyon hatasi kullanicinin kayit islemini bozmaz; yalnizca loglanir.
/// </summary> /// </summary>
private async Task ExecuteAsync( private Task ExecuteAsync(
string listFormCode,
Func<ListForm, bool> isEnabled,
Func<ListForm, SeedTarget, Task> action)
{
if (string.IsNullOrWhiteSpace(listFormCode))
return Task.CompletedTask;
// Satir EF ile yazildiysa (MenuAppService, CustomComponentAppService) degisiklik commit
// edilmeden select ayri baglantidan eski degeri okur; dosya bu yuzden commit sonrasinda,
// kendi UoW'sinde yazilir. Geri alinan islem dosyaya hic yansimaz.
var unitOfWork = unitOfWorkManager.Current;
if (unitOfWork == null)
return RunAsync(listFormCode, isEnabled, action);
unitOfWork.OnCompleted(async () =>
{
using var syncUnitOfWork = unitOfWorkManager.Begin(requiresNew: true);
await RunAsync(listFormCode, isEnabled, action);
await syncUnitOfWork.CompleteAsync();
});
return Task.CompletedTask;
}
private async Task RunAsync(
string listFormCode, string listFormCode,
Func<ListForm, bool> isEnabled, Func<ListForm, bool> isEnabled,
Func<ListForm, SeedTarget, Task> action, Func<ListForm, SeedTarget, Task> action,
bool createTarget = true) bool createTarget = true)
{ {
if (string.IsNullOrWhiteSpace(listFormCode))
return;
try try
{ {
var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode); var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);

View file

@ -34,6 +34,7 @@ public class MenuAppService : CrudAppService<
private readonly IPermissionDefinitionRecordRepository _permissionRepository; private readonly IPermissionDefinitionRecordRepository _permissionRepository;
private readonly LanguageTextAppService _languageTextAppService; private readonly LanguageTextAppService _languageTextAppService;
private readonly IRepository<Module, string> _moduleRepository; private readonly IRepository<Module, string> _moduleRepository;
private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer;
public MenuAppService( public MenuAppService(
IRepository<Menu, Guid> menuRepository, IRepository<Menu, Guid> menuRepository,
@ -42,7 +43,8 @@ public class MenuAppService : CrudAppService<
ITenantRepository tenantRepository, ITenantRepository tenantRepository,
IPermissionDefinitionRecordRepository permissionRepository, IPermissionDefinitionRecordRepository permissionRepository,
LanguageTextAppService languageTextAppService, LanguageTextAppService languageTextAppService,
IRepository<Module, string> moduleRepository IRepository<Module, string> moduleRepository,
IListFormSeedDataSynchronizer seedDataSynchronizer
) : base(menuRepository) ) : base(menuRepository)
{ {
_menuRepository = menuRepository; _menuRepository = menuRepository;
@ -52,6 +54,7 @@ public class MenuAppService : CrudAppService<
_permissionRepository = permissionRepository; _permissionRepository = permissionRepository;
_languageTextAppService = languageTextAppService; _languageTextAppService = languageTextAppService;
_moduleRepository = moduleRepository; _moduleRepository = moduleRepository;
_seedDataSynchronizer = seedDataSynchronizer;
CreatePolicyName = $"{AppCodes.Menus.Menu}.Create"; CreatePolicyName = $"{AppCodes.Menus.Menu}.Create";
UpdatePolicyName = $"{AppCodes.Menus.Menu}.Update"; UpdatePolicyName = $"{AppCodes.Menus.Menu}.Update";
@ -182,7 +185,9 @@ public class MenuAppService : CrudAppService<
}); });
} }
return await base.CreateAsync(input); var result = await base.CreateAsync(input);
await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Insert);
return result;
} }
public override async Task<MenuDto> UpdateAsync(Guid id, MenuDto input) public override async Task<MenuDto> UpdateAsync(Guid id, MenuDto input)
@ -205,7 +210,10 @@ public class MenuAppService : CrudAppService<
}); });
} }
return await base.UpdateAsync(id, input); var previousCode = (await _menuRepository.GetAsync(id)).Code;
var result = await base.UpdateAsync(id, input);
await SyncUpdatedMenuRowsAsync([(previousCode, result.Code)]);
return result;
} }
public async Task<List<MenuDto>> UpdateAllAsync(List<MenuDto> inputs) public async Task<List<MenuDto>> UpdateAllAsync(List<MenuDto> inputs)
@ -246,6 +254,10 @@ public class MenuAppService : CrudAppService<
await _repositoryKey.InsertManyAsync(newKeys, autoSave: true); await _repositoryKey.InsertManyAsync(newKeys, autoSave: true);
} }
var ids = inputs.Select(x => x.Id).ToList();
var previousCodes = (await _menuRepository.GetListAsync(a => ids.Contains(a.Id)))
.ToDictionary(a => a.Id, a => a.Code);
// Menüleri güncelle // Menüleri güncelle
var result = new List<MenuDto>(); var result = new List<MenuDto>();
foreach (var input in inputs) foreach (var input in inputs)
@ -254,6 +266,8 @@ public class MenuAppService : CrudAppService<
result.Add(updated); result.Add(updated);
} }
await SyncUpdatedMenuRowsAsync(result.Select(x => (previousCodes.GetValueOrDefault(x.Id), x.Code)));
return result; return result;
} }
@ -270,6 +284,9 @@ public class MenuAppService : CrudAppService<
} }
await base.DeleteAsync(id); await base.DeleteAsync(id);
if (menu != null)
await _seedDataSynchronizer.RemoveRowsAsync(AppCodes.Menus.Menu, [menu.Code]);
} }
public async Task<List<MenuDto>> GetListMainMenuAsync() public async Task<List<MenuDto>> GetListMainMenuAsync()
@ -307,7 +324,9 @@ public class MenuAppService : CrudAppService<
await UpsertMenuLanguageTextsAsync(menu.Code, input.MenuTextEn, input.MenuTextTr); await UpsertMenuLanguageTextsAsync(menu.Code, input.MenuTextEn, input.MenuTextTr);
return await base.UpdateAsync(id, input); var result = await base.UpdateAsync(id, input);
await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Update);
return result;
} }
/// <summary> /// <summary>
@ -327,6 +346,9 @@ public class MenuAppService : CrudAppService<
}, autoSave: true); }, autoSave: true);
} }
var changedTextIds = new List<object>();
var insertedTextIds = new List<object>();
foreach (var (culture, value) in new[] { ("en", menuTextEn), ("tr", menuTextTr) }) foreach (var (culture, value) in new[] { ("en", menuTextEn), ("tr", menuTextTr) })
{ {
if (value == null) if (value == null)
@ -341,20 +363,27 @@ public class MenuAppService : CrudAppService<
{ {
existing.Value = value; existing.Value = value;
await _repositoryText.UpdateAsync(existing, autoSave: true); await _repositoryText.UpdateAsync(existing, autoSave: true);
changedTextIds.Add(existing.Id);
} }
else else
{ {
await _repositoryText.InsertAsync(new LanguageText var inserted = await _repositoryText.InsertAsync(new LanguageText
{ {
Key = code, Key = code,
CultureName = culture, CultureName = culture,
Value = value, Value = value,
ResourceName = PlatformConsts.AppName ResourceName = PlatformConsts.AppName
}, autoSave: true); }, autoSave: true);
insertedTextIds.Add(inserted.Id);
} }
} }
await _languageTextAppService.ClearRedisCacheAsync(); await _languageTextAppService.ClearRedisCacheAsync();
// Dil metinleri de kendi ekraninin seed dosyasinda yasar; menu adini runtime'da
// degistirmek DB sifirlandiginda geri gelmeli.
await _seedDataSynchronizer.SyncRowsAsync(AppCodes.Languages.LanguageText, changedTextIds, SeedSyncOperation.Update);
await _seedDataSynchronizer.SyncRowsAsync(AppCodes.Languages.LanguageText, insertedTextIds, SeedSyncOperation.Insert);
} }
public async Task<MenuDto> CreateWithLanguageKeyTextAsync(MenuDto input) public async Task<MenuDto> CreateWithLanguageKeyTextAsync(MenuDto input)
@ -363,67 +392,36 @@ public class MenuAppService : CrudAppService<
await EnsureModuleAsync(input.ModuleId); await EnsureModuleAsync(input.ModuleId);
//Dil Key oluşturuluyor. await UpsertMenuLanguageTextsAsync(input.Code, input.MenuTextEn, input.MenuTextTr);
var keyExists = await _repositoryKey.AnyAsync(
a => a.Key == input.Code && var result = await base.CreateAsync(input);
a.ResourceName == PlatformConsts.AppName); await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Insert);
if (!keyExists) return result;
{
await _repositoryKey.InsertAsync(new LanguageKey
{
Key = input.Code,
ResourceName = PlatformConsts.AppName
}, autoSave: true);
} }
// English text oluşturuluyor veya güncelleniyor. /// <summary>
var existingEnText = await _repositoryText.FirstOrDefaultAsync( /// Menu Yoneticisi de <c>App.Menus.Menu</c> ekraniyla ayni seed dosyasini yazar; aksi halde
a => a.CultureName == "en" && /// siralama ve duzenlemeler DB sifirlandiginda kaybolur. Dosyanin anahtari <c>Code</c>'dur.
a.Key == input.Code && /// </summary>
a.ResourceName == PlatformConsts.AppName); private Task SyncMenuRowsAsync(IReadOnlyCollection<string> codes, SeedSyncOperation operation) =>
_seedDataSynchronizer.SyncRowsAsync(AppCodes.Menus.Menu, [.. codes], operation);
if (existingEnText != null) /// <summary>
/// Kodu degisen menunun eski satiri dosyadan cikarilir; dosya Code anahtarli oldugu icin
/// yeni kod ayri bir satir olarak eklenir.
/// </summary>
private async Task SyncUpdatedMenuRowsAsync(IEnumerable<(string PreviousCode, string Code)> changes)
{ {
existingEnText.Value = input.MenuTextEn; var list = changes.ToList();
await _repositoryText.UpdateAsync(existingEnText, autoSave: true);
}
else
{
await _repositoryText.InsertAsync(new LanguageText
{
Key = input.Code,
CultureName = "en",
Value = input.MenuTextEn,
ResourceName = PlatformConsts.AppName
}, autoSave: true);
}
// Türkçe text oluşturuluyor veya güncelleniyor. var renamed = list
var existingTrText = await _repositoryText.FirstOrDefaultAsync( .Where(x => !string.IsNullOrWhiteSpace(x.PreviousCode) && x.PreviousCode != x.Code)
a => a.CultureName == "tr" && .Select(x => (object)x.PreviousCode)
a.Key == input.Code && .ToArray();
a.ResourceName == PlatformConsts.AppName); if (renamed.Length > 0)
await _seedDataSynchronizer.RemoveRowsAsync(AppCodes.Menus.Menu, renamed);
if (existingTrText != null) await SyncMenuRowsAsync([.. list.Select(x => x.Code)], SeedSyncOperation.Update);
{
existingTrText.Value = input.MenuTextTr;
await _repositoryText.UpdateAsync(existingTrText, autoSave: true);
}
else
{
await _repositoryText.InsertAsync(new LanguageText
{
Key = input.Code,
CultureName = "tr",
Value = input.MenuTextTr,
ResourceName = PlatformConsts.AppName
}, autoSave: true);
}
// Clear Redis Cache
await _languageTextAppService.ClearRedisCacheAsync();
return await base.CreateAsync(input);
} }
/// <summary> /// <summary>

View file

@ -406,7 +406,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
Migrate/seed sırasında ve wizard import'unun kapanışında Migrate/seed sırasında ve wizard import'unun kapanışında
(`CompleteImport`) `ListFormSeedDataApplier` dosyayı geri uygular. Migrate/seed'de kayıt yoksa (`CompleteImport`) `ListFormSeedDataApplier` dosyayı geri uygular. Migrate/seed'de kayıt yoksa
eklenir, varsa dokunulmaz; import'ta `overwriteExisting` açıktır: var olan kayıt dosyadaki eklenir, varsa dokunulmaz; import'ta `overwriteExisting` açıktır: var olan kayıt dosyadaki
değerlerle güncellenir ve soft delete izleri (`IsDeleted`/`DeletionTime`/`DeleterId`) temizlenir. Yeni bir kayıt yolu (yeni endpoint, toplu işlem, tipli AppService) eklenirse değerlerle güncellenir ve soft delete izleri (`IsDeleted`/`DeletionTime`/`DeleterId`) temizlenir. Satırı veritabanından okuyan senkronizasyon açık UoW varsa commit sonrasında kendi UoW'sinde çalışır (EF ile yazan çağıran, ör. `MenuAppService`, aksi hâlde eski değeri dosyaya yazardı); yalnızca dosyaya dokunan `RemoveRowsByFieldAsync` hemen çalışır. Yeni bir kayıt yolu (yeni endpoint, toplu işlem, tipli AppService) eklenirse
senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar. senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar.
- **Seed dosyası yazmak** → veri dosyası `ListFormSeedDataFileDto.Serialize`, wizard dosyası - **Seed dosyası yazmak** → veri dosyası `ListFormSeedDataFileDto.Serialize`, wizard dosyası
`WizardSeedFileDto.ToSeedJson` ile yazılır; düz `JsonSerializer.Serialize` + `File.WriteAllText` `WizardSeedFileDto.ToSeedJson` ile yazılır; düz `JsonSerializer.Serialize` + `File.WriteAllText`

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff