From 9816fb91cec4d6cf914b7ea1fa549dc9eaba56a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?=
<76204082+iamsedatozturk@users.noreply.github.com>
Date: Thu, 24 Sep 2026 17:24:37 +0300
Subject: [PATCH] Wizard Delete eksiklikleri
---
.github/instructions/lowcode.instructions.md | 15 ++
README.md | 3 +
.../ListForms/ListFormSeedDataSynchronizer.cs | 53 ++++++-
.../ListForms/ListFormWizardAppService.cs | 9 +-
.../ListForms/WizardRecordCleaner.cs | 146 ++++++++++++++++--
.../Migrations/WizardDataSeeder.cs | 25 +--
.../Extensions/AbpPermissionsExtensions.cs | 16 ++
7 files changed, 238 insertions(+), 29 deletions(-)
diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md
index 9e5e10e3..0c15ccac 100644
--- a/.github/instructions/lowcode.instructions.md
+++ b/.github/instructions/lowcode.instructions.md
@@ -1469,6 +1469,21 @@ kullanan, sonra kullanılan — ve her kontrol o ana kadar kalan kayıtlara kar
| Veri kaynağı | ekran, Custom Endpoint, zamanlanmış iş |
| Dil anahtarı | menü/yetki/yetki grubu adı, ekran başlığı/açıklaması, alan başlığı |
+Silme yolunda wizard'ın **bağlandığı** kaplar da boşaldıysa gider, `InsertedRecords`'ta olmasalar
+bile: kap başka bir wizard'la oluşmuş ve o wizard silinirken bu wizard yüzünden bırakılmış
+olabilir. Üst menü zinciri yukarı doğru yürünür — alt menüsü kalmayan ve kendisi adres açmayan
+(`Url` boş) menü silinir, adresi olan ya da altında menü kalan kayıtta zincir durur. Wizard'ın
+`PermissionGroupName`'i içinde yetki kalmamışsa, silinen menülerin ve `MenuParentModuleId`'nin
+modülü yukarıdaki tabloda kullananı yoksa silinir; adlarının dil anahtarları da aynı kontrolden
+geçer. Bu kaplar rapora yalnızca silindiklerinde girer. Düzenleme (redeploy) yolunda kaplara
+dokunulmaz.
+
+Silinen menü, modül, yetki ve yetki grubu satırları ekranların seed dosyalarından da çıkarılır
+(`App.Menus.{Menu,Module}.json`, `Abp.Identity.Permission{s,Groups}.json`;
+`IListFormSeedDataSynchronizer.RemoveRowsByFieldAsync`, iş anahtarıyla, `SeedSyncDelete`'e
+bağlı). Wizard bu dosyalara yazmaz, ama kayıt menü/yetki ekranlarından düzenlendiğinde satır
+senkronizasyonla girer; dosyada kalırsa migrate kaydı geri getirir.
+
Bağlı wizard'ı olan (alt formu ya da component bağımlılığı wizard olarak duran) bir wizard
silinemez; önce bağlı olanlar silinir. Kural listedeki gruplamayla aynı kaynaktandır
(`WizardFileInfoDto.Dependencies`), sunucu `DeleteFile`'da reddeder, istemci butonu kapatır.
diff --git a/README.md b/README.md
index 29c66522..78128fd8 100644
--- a/README.md
+++ b/README.md
@@ -911,6 +911,9 @@ Sıra **kardeşler arasındadır**: her üst yetkinin altında 1'den başlar, gr
kendi aralarında 1'den. `PlatformPermissionAppService` sıralama anahtarını kökten yaprağa üst yetki
zincirinden kurar, böylece çocuk her zaman ebeveyninin altında kalır. Değerler seed dosyalarındaki
`Order` alanında tutulur ve Permissions / Permission Groups ekranlarından düzenlenebilir.
+Wizard (ve migrate'te `WizardDataSeeder`) yeni grubu ve grubun yeni kök yetkisini kardeşlerin en
+büyük sırasının bir fazlasıyla, alt yetkileri Create/Update/Delete/Export/Import/Note dizilimiyle
+1'den yazar (`NextPermissionOrder`, `AbpPermissionsExtensions`).
Aksiyon yetkisi olan alt gruplar (UI tarafındaki karşılıkları `constants/permission.constant.ts`
içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppService'lerdedir):
diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSeedDataSynchronizer.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSeedDataSynchronizer.cs
index 9897a6ca..80afefc6 100644
--- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSeedDataSynchronizer.cs
+++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSeedDataSynchronizer.cs
@@ -31,6 +31,15 @@ public interface IListFormSeedDataSynchronizer
/// Silinen satirlari dosyadan cikarir; SeedSyncDelete kapaliysa hicbir sey yapmaz.
Task RemoveRowsAsync(string listFormCode, object[] keys);
+ ///
+ /// alaninin degeri icinde olan satirlari
+ /// dosyadan cikarir; SeedSyncDelete kapaliysa ya da hedef diskte yoksa hicbir sey yapmaz.
+ /// Kaydi veritabani disinda silinmis satirlar icindir: eslesme is anahtari uzerinden yapilir,
+ /// cunku dosyanin anahtari ekraninkinden farkli olabilir (yetki dosyalari Name ile
+ /// yazilir, ekran Id kullanir) ve dosyanin kendi basligina dokunulmaz.
+ ///
+ Task RemoveRowsByFieldAsync(string listFormCode, string fieldName, IReadOnlyCollection values);
+
///
/// Dosyayi listenin tamamiyla yeniden yazar; toplu islemler (import) icin kullanilir.
/// Toplu yukleme kayit ekledigi icin SeedSyncInsert bayragina baglidir.
@@ -149,6 +158,44 @@ public class ListFormSeedDataSynchronizer(
});
}
+ public async Task RemoveRowsByFieldAsync(string listFormCode, string fieldName, IReadOnlyCollection values)
+ {
+ var wanted = (values ?? [])
+ .Where(v => !string.IsNullOrWhiteSpace(v))
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ if (string.IsNullOrWhiteSpace(fieldName) || wanted.Count == 0)
+ return;
+
+ bool Matches(Dictionary row) =>
+ row.Any(kv => string.Equals(kv.Key, fieldName, StringComparison.OrdinalIgnoreCase) &&
+ wanted.Contains(ToComparableText(kv.Value)));
+
+ await ExecuteAsync(listFormCode, listForm => listForm.SeedSyncDelete, async (_, target) =>
+ {
+ List files = target.IsFolder
+ ? Directory.Exists(target.Path) ? [.. Directory.EnumerateFiles(target.Path, "*.json")] : []
+ : File.Exists(target.Path) ? [target.Path] : [];
+
+ foreach (var filePath in files)
+ {
+ var seedFile = JsonSerializer.Deserialize(
+ await File.ReadAllTextAsync(filePath),
+ ListFormSeedDataFileDto.JsonOptions);
+
+ if (seedFile?.Rows == null || seedFile.Rows.RemoveAll(Matches) == 0)
+ continue;
+
+ // Klasor modunda satir dosyasi tek satirliktir; bosalan dosya klasorde kalirsa
+ // applier onu satirsiz bir ekran dosyasi olarak okur.
+ if (target.IsFolder && seedFile.Rows.Count == 0)
+ File.Delete(filePath);
+ else
+ await WriteSeedFileAsync(filePath, seedFile);
+ }
+ }, createTarget: false);
+ }
+
public async Task SyncAllAsync(string listFormCode)
{
await ExecuteAsync(listFormCode, listForm => listForm.SeedSyncInsert, WriteAllRowsAsync);
@@ -290,7 +337,8 @@ public class ListFormSeedDataSynchronizer(
private async Task ExecuteAsync(
string listFormCode,
Func isEnabled,
- Func action)
+ Func action,
+ bool createTarget = true)
{
if (string.IsNullOrWhiteSpace(listFormCode))
return;
@@ -309,7 +357,8 @@ public class ListFormSeedDataSynchronizer(
listForm.SeedFilePath.Replace('\\', '/').Trim('/').Split('/'));
// Klasor ilk kayitta olusur; ekran, hedefi diskte hazir bulmak zorunda degildir.
- Directory.CreateDirectory(isFolder ? fullPath : Path.GetDirectoryName(fullPath)!);
+ if (createTarget)
+ Directory.CreateDirectory(isFolder ? fullPath : Path.GetDirectoryName(fullPath)!);
await action(listForm, new SeedTarget(fullPath, isFolder));
}
diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs
index bcc7710d..df8a7387 100644
--- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs
+++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs
@@ -188,6 +188,7 @@ public class ListFormWizardAppService(
{
var permReadRecord = new PermissionDefinitionRecord(GuidGenerator.Create(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both);
permReadRecord.SetModules(permissionModules);
+ permReadRecord.SetPermissionOrder(existingPerms.Where(p => string.IsNullOrWhiteSpace(p.ParentName)).NextPermissionOrder());
permRead = await repoPerm.InsertAsync(permReadRecord, autoSave: false);
inserted.PermissionNames.Add(permRead.Name);
@@ -217,16 +218,20 @@ public class ListFormWizardAppService(
: [.. crudPermissions, .. listOnlyPermissions];
var permissionNames = new List { permRead.Name };
+ // Alt yetkilerin sirasi kardesler arasindadir ve listedeki dizilimi izler (Create = 1, ...).
+ var childOrder = 0;
foreach (var (suffix, langKey) in childPermissions)
{
var permissionName = $"{code}.{suffix}";
permissionNames.Add(permissionName);
+ childOrder++;
if (existingPerms.Any(a => a.Name == permissionName))
continue;
var childPermission = new PermissionDefinitionRecord(GuidGenerator.Create(), groupName, permissionName, permRead.Name, langKey, true, MultiTenancySides.Both);
childPermission.SetModules(permissionModules);
+ childPermission.SetPermissionOrder(childOrder);
await repoPerm.InsertAsync(childPermission, autoSave: false);
inserted.PermissionNames.Add(permissionName);
@@ -2306,7 +2311,9 @@ public class ListFormWizardAppService(
if (!await repoPermGroup.AnyAsync(a => a.Name == groupName))
{
- await repoPermGroup.InsertAsync(new PermissionGroupDefinitionRecord(GuidGenerator.Create(), groupName, groupName), autoSave: false);
+ var group = new PermissionGroupDefinitionRecord(GuidGenerator.Create(), groupName, groupName);
+ group.SetPermissionOrder((await repoPermGroup.GetListAsync()).NextPermissionOrder());
+ await repoPermGroup.InsertAsync(group, autoSave: false);
// Grup adi ust menu koduyla ayniysa anahtar menu adiyla ortaktir; metin varsa uzerine
// yazmak menu adini bozar, ust menu daha once baska bir wizard'la olustuysa metin hic
diff --git a/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs b/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs
index 925bbdfe..f2c197c9 100644
--- a/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs
+++ b/api/src/Sozsoft.Platform.Application/ListForms/WizardRecordCleaner.cs
@@ -33,6 +33,13 @@ namespace Sozsoft.Platform.ListForms;
/// kaynagi) ve diger rollerin yetki atamalari korunur; silinip yeniden uretilseler menu sirasi ve
/// rol atamalari kaybolurdu.
///
+///
+/// Silme yolunda wizard'in baglandigi kaplar da (ust menu zinciri, modul, izin grubu) bosaldiysa
+/// silinir: kap bir baska wizard'la olusmus ve o wizard silinirken bu wizard'in menusu yuzunden
+/// birakilmis olabilir; sahibi kalmayan kap yeniden deploy'da artik olarak kalirdi. Bu kaplar
+/// yalnizca silindiklerinde rapora girer. Silinen menu, modul, yetki ve izin grubu satirlari ekranlarin
+/// seed dosyalarindan da cikarilir; aksi halde migrate onlari geri getirirdi.
+///
///
public class WizardRecordCleaner(
IRepository repoListForm,
@@ -50,6 +57,7 @@ public class WizardRecordCleaner(
IRepository repoBackgroundWorker,
IRepository repoIntranetWidget,
ITenantRepository tenantRepository,
+ IListFormSeedDataSynchronizer seedDataSynchronizer,
IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger logger) : ITransientDependency
@@ -67,12 +75,38 @@ public class WizardRecordCleaner(
if (wizard?.ComponentKind != WizardComponentKindEnum.Custom && !string.IsNullOrWhiteSpace(wizard?.ListFormCode))
await CleanListFormAsync(wizard.ListFormCode, mode, report);
- await CleanMenusAsync(ins.MenuCodes, wizard?.MenuParentCode, mode, report);
+ var deletedMenus = await CleanMenusAsync(ins.MenuCodes, wizard?.MenuParentCode, mode, report);
+ var adoptedLanguageKeys = new List();
+
+ if (mode == WizardDeleteMode.Delete && wizard?.CreateMenu == true)
+ {
+ var parents = deletedMenus.Select(m => m.ParentCode).Append(wizard.MenuParentCode);
+ var emptyParents = await CleanEmptyParentMenusAsync(parents, deletedMenus, report);
+ deletedMenus.AddRange(emptyParents);
+ adoptedLanguageKeys.AddRange(emptyParents.Select(m => m.DisplayName));
+ }
+
await CleanPermissionsAsync(ins.PermissionNames, mode, report);
- await CleanPermissionGroupsAsync(ins.PermissionGroupNames, mode, report);
- await CleanModulesAsync(ins.ModuleIds, mode, report);
+
+ await CleanPermissionGroupsAsync(ins.PermissionGroupNames, mode, report, reportKept: true);
+ await CleanModulesAsync(ins.ModuleIds, mode, report, reportKept: true);
+
+ if (mode == WizardDeleteMode.Delete)
+ {
+ var groups = await CleanPermissionGroupsAsync(
+ Adopted([wizard?.PermissionGroupName], ins.PermissionGroupNames), mode, report, reportKept: false);
+ adoptedLanguageKeys.AddRange(groups);
+
+ await CleanModulesAsync(
+ Adopted([.. deletedMenus.Select(m => m.ModuleId), wizard?.MenuParentModuleId], ins.ModuleIds),
+ mode, report, reportKept: false);
+ }
+
await CleanDataSourcesAsync(ins.DataSourceCodes, mode, report);
- await CleanLanguageKeysAsync(ins.LanguageKeys, report);
+ await CleanLanguageKeysAsync(ins.LanguageKeys, report, reportKept: true);
+ await CleanLanguageKeysAsync(Adopted(adoptedLanguageKeys, ins.LanguageKeys), report, reportKept: false);
+
+ await RemoveSeedRowsAsync(report);
foreach (var item in report)
{
@@ -188,8 +222,9 @@ public class WizardRecordCleaner(
/// Altinda baska menu kalan menu silinmez. Wizard'in kendi menusu ust menuden once silinir;
/// ust menunun bos kalip kalmadigi ancak o zaman bilinir.
///
- private async Task CleanMenusAsync(List codes, string? menuParentCode, WizardDeleteMode mode, List report)
+ private async Task> CleanMenusAsync(List codes, string? menuParentCode, WizardDeleteMode mode, List report)
{
+ var deleted = new List