MenuManager komponenti json dosyası güncellenebiliyor.
This commit is contained in:
parent
61706220b1
commit
c4e90a459f
7 changed files with 690 additions and 214 deletions
5
.github/instructions/lowcode.instructions.md
vendored
5
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -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
|
||||
yalnızca fiziksel React sayfaları içindir ve prompt ile ekran üretirken böyle bir sayfa
|
||||
yazılmaz.
|
||||
3. **Menü sırası** için wizard'ın `MenuOrder` alanını kullan; Menu Manager'da elle taşımak seed
|
||||
dosyasına yansımaz ve veritabanı sıfırlandığında kaybolur.
|
||||
3. **Menü sırası** için wizard'ın `MenuOrder` alanını kullan. Menu Manager'daki taşıma/düzenleme de
|
||||
`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 —
|
||||
aynı `Url`, farklı `RoleId`/`CultureName`. Wizard bunu üretmez.
|
||||
|
||||
|
|
|
|||
|
|
@ -899,7 +899,7 @@ Route.Authority ──┘ └► Permission
|
|||
- **Menu** (`Menu.cs`): `Code`, `ParentCode`, `DisplayName`, `Url`, `Icon`, `Order`,
|
||||
`RequiredPermissionName`, `Target`, `IsDisabled`, ayrıca `UserId` / `RoleId` / `CultureName`
|
||||
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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using Sozsoft.Platform.ListForms.Select;
|
|||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
using Volo.Abp.Uow;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
|
|
@ -28,6 +29,12 @@ public interface IListFormSeedDataSynchronizer
|
|||
/// </summary>
|
||||
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>
|
||||
Task RemoveRowsAsync(string listFormCode, object[] keys);
|
||||
|
||||
|
|
@ -70,6 +77,7 @@ public class ListFormSeedDataSynchronizer(
|
|||
IListFormSelectAppService listFormSelectAppService,
|
||||
IConfiguration configuration,
|
||||
ICurrentTenant currentTenant,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
ILogger<ListFormSeedDataSynchronizer> logger) : IListFormSeedDataSynchronizer, ITransientDependency
|
||||
{
|
||||
private const int SyncAllPageSize = 10000;
|
||||
|
|
@ -80,33 +88,51 @@ public class ListFormSeedDataSynchronizer(
|
|||
/// </summary>
|
||||
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) =>
|
||||
{
|
||||
// Insert ucu her veri kaynaginda anahtari geri dondurmez; anahtarsiz kaldigimizda
|
||||
// ya da satir okunamadiginda hedef listenin tamamiyla yenilenir, bayat kalmaz.
|
||||
var row = key == null ? null : await ReadRowAsync(listForm, key);
|
||||
if (row == null)
|
||||
var rows = new List<(object Key, Dictionary<string, object> Row)>();
|
||||
foreach (var key in keys ?? [])
|
||||
{
|
||||
logger.LogInformation("Row '{Key}' could not be read for '{ListFormCode}', seed file is rewritten from the list.", key, listForm.ListFormCode);
|
||||
await WriteAllRowsAsync(listForm, target);
|
||||
return;
|
||||
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, target);
|
||||
return;
|
||||
}
|
||||
|
||||
rows.Add((key, row));
|
||||
}
|
||||
|
||||
if (rows.Count == 0)
|
||||
return;
|
||||
|
||||
if (target.IsFolder)
|
||||
{
|
||||
await WriteRowFileAsync(listForm, target, key, row);
|
||||
foreach (var (key, row) in rows)
|
||||
await WriteRowFileAsync(listForm, target, key, row);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var seedFile = await ReadSeedFileAsync(target.Path, listForm);
|
||||
var index = FindRowIndex(seedFile, listForm.KeyFieldName, key);
|
||||
|
||||
if (index >= 0)
|
||||
seedFile.Rows[index] = row;
|
||||
else
|
||||
seedFile.Rows.Add(row);
|
||||
foreach (var (key, row) in rows)
|
||||
{
|
||||
var index = FindRowIndex(seedFile, listForm.KeyFieldName, key);
|
||||
|
||||
if (index >= 0)
|
||||
seedFile.Rows[index] = row;
|
||||
else
|
||||
seedFile.Rows.Add(row);
|
||||
}
|
||||
|
||||
await WriteSeedFileAsync(target.Path, seedFile);
|
||||
});
|
||||
|
|
@ -171,7 +197,9 @@ public class ListFormSeedDataSynchronizer(
|
|||
row.Any(kv => string.Equals(kv.Key, fieldName, StringComparison.OrdinalIgnoreCase) &&
|
||||
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
|
||||
? 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.
|
||||
/// Senkronizasyon hatasi kullanicinin kayit islemini bozmaz; yalnizca loglanir.
|
||||
/// </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,
|
||||
Func<ListForm, bool> isEnabled,
|
||||
Func<ListForm, SeedTarget, Task> action,
|
||||
bool createTarget = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(listFormCode))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ public class MenuAppService : CrudAppService<
|
|||
private readonly IPermissionDefinitionRecordRepository _permissionRepository;
|
||||
private readonly LanguageTextAppService _languageTextAppService;
|
||||
private readonly IRepository<Module, string> _moduleRepository;
|
||||
private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer;
|
||||
|
||||
public MenuAppService(
|
||||
IRepository<Menu, Guid> menuRepository,
|
||||
|
|
@ -42,7 +43,8 @@ public class MenuAppService : CrudAppService<
|
|||
ITenantRepository tenantRepository,
|
||||
IPermissionDefinitionRecordRepository permissionRepository,
|
||||
LanguageTextAppService languageTextAppService,
|
||||
IRepository<Module, string> moduleRepository
|
||||
IRepository<Module, string> moduleRepository,
|
||||
IListFormSeedDataSynchronizer seedDataSynchronizer
|
||||
) : base(menuRepository)
|
||||
{
|
||||
_menuRepository = menuRepository;
|
||||
|
|
@ -52,6 +54,7 @@ public class MenuAppService : CrudAppService<
|
|||
_permissionRepository = permissionRepository;
|
||||
_languageTextAppService = languageTextAppService;
|
||||
_moduleRepository = moduleRepository;
|
||||
_seedDataSynchronizer = seedDataSynchronizer;
|
||||
|
||||
CreatePolicyName = $"{AppCodes.Menus.Menu}.Create";
|
||||
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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -246,6 +254,10 @@ public class MenuAppService : CrudAppService<
|
|||
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
|
||||
var result = new List<MenuDto>();
|
||||
foreach (var input in inputs)
|
||||
|
|
@ -254,6 +266,8 @@ public class MenuAppService : CrudAppService<
|
|||
result.Add(updated);
|
||||
}
|
||||
|
||||
await SyncUpdatedMenuRowsAsync(result.Select(x => (previousCodes.GetValueOrDefault(x.Id), x.Code)));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -270,6 +284,9 @@ public class MenuAppService : CrudAppService<
|
|||
}
|
||||
|
||||
await base.DeleteAsync(id);
|
||||
|
||||
if (menu != null)
|
||||
await _seedDataSynchronizer.RemoveRowsAsync(AppCodes.Menus.Menu, [menu.Code]);
|
||||
}
|
||||
|
||||
public async Task<List<MenuDto>> GetListMainMenuAsync()
|
||||
|
|
@ -307,7 +324,9 @@ public class MenuAppService : CrudAppService<
|
|||
|
||||
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>
|
||||
|
|
@ -327,6 +346,9 @@ public class MenuAppService : CrudAppService<
|
|||
}, autoSave: true);
|
||||
}
|
||||
|
||||
var changedTextIds = new List<object>();
|
||||
var insertedTextIds = new List<object>();
|
||||
|
||||
foreach (var (culture, value) in new[] { ("en", menuTextEn), ("tr", menuTextTr) })
|
||||
{
|
||||
if (value == null)
|
||||
|
|
@ -341,20 +363,27 @@ public class MenuAppService : CrudAppService<
|
|||
{
|
||||
existing.Value = value;
|
||||
await _repositoryText.UpdateAsync(existing, autoSave: true);
|
||||
changedTextIds.Add(existing.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _repositoryText.InsertAsync(new LanguageText
|
||||
var inserted = await _repositoryText.InsertAsync(new LanguageText
|
||||
{
|
||||
Key = code,
|
||||
CultureName = culture,
|
||||
Value = value,
|
||||
ResourceName = PlatformConsts.AppName
|
||||
}, autoSave: true);
|
||||
insertedTextIds.Add(inserted.Id);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -363,67 +392,36 @@ public class MenuAppService : CrudAppService<
|
|||
|
||||
await EnsureModuleAsync(input.ModuleId);
|
||||
|
||||
//Dil Key oluşturuluyor.
|
||||
var keyExists = await _repositoryKey.AnyAsync(
|
||||
a => a.Key == input.Code &&
|
||||
a.ResourceName == PlatformConsts.AppName);
|
||||
if (!keyExists)
|
||||
{
|
||||
await _repositoryKey.InsertAsync(new LanguageKey
|
||||
{
|
||||
Key = input.Code,
|
||||
ResourceName = PlatformConsts.AppName
|
||||
}, autoSave: true);
|
||||
}
|
||||
await UpsertMenuLanguageTextsAsync(input.Code, input.MenuTextEn, input.MenuTextTr);
|
||||
|
||||
// English text oluşturuluyor veya güncelleniyor.
|
||||
var existingEnText = await _repositoryText.FirstOrDefaultAsync(
|
||||
a => a.CultureName == "en" &&
|
||||
a.Key == input.Code &&
|
||||
a.ResourceName == PlatformConsts.AppName);
|
||||
var result = await base.CreateAsync(input);
|
||||
await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Insert);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (existingEnText != null)
|
||||
{
|
||||
existingEnText.Value = input.MenuTextEn;
|
||||
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);
|
||||
}
|
||||
/// <summary>
|
||||
/// Menu Yoneticisi de <c>App.Menus.Menu</c> ekraniyla ayni seed dosyasini yazar; aksi halde
|
||||
/// siralama ve duzenlemeler DB sifirlandiginda kaybolur. Dosyanin anahtari <c>Code</c>'dur.
|
||||
/// </summary>
|
||||
private Task SyncMenuRowsAsync(IReadOnlyCollection<string> codes, SeedSyncOperation operation) =>
|
||||
_seedDataSynchronizer.SyncRowsAsync(AppCodes.Menus.Menu, [.. codes], operation);
|
||||
|
||||
// Türkçe text oluşturuluyor veya güncelleniyor.
|
||||
var existingTrText = await _repositoryText.FirstOrDefaultAsync(
|
||||
a => a.CultureName == "tr" &&
|
||||
a.Key == input.Code &&
|
||||
a.ResourceName == PlatformConsts.AppName);
|
||||
/// <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)
|
||||
{
|
||||
var list = changes.ToList();
|
||||
|
||||
if (existingTrText != null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
var renamed = list
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.PreviousCode) && x.PreviousCode != x.Code)
|
||||
.Select(x => (object)x.PreviousCode)
|
||||
.ToArray();
|
||||
if (renamed.Length > 0)
|
||||
await _seedDataSynchronizer.RemoveRowsAsync(AppCodes.Menus.Menu, renamed);
|
||||
|
||||
// Clear Redis Cache
|
||||
await _languageTextAppService.ClearRedisCacheAsync();
|
||||
|
||||
return await base.CreateAsync(input);
|
||||
await SyncMenuRowsAsync([.. list.Select(x => x.Code)], SeedSyncOperation.Update);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
(`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
|
||||
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.
|
||||
- **Seed dosyası yazmak** → veri dosyası `ListFormSeedDataFileDto.Serialize`, wizard dosyası
|
||||
`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
Loading…
Reference in a new issue