WizardFileManager silme işlemi
This commit is contained in:
parent
488a3274e1
commit
197b3775f8
27 changed files with 1009 additions and 12784 deletions
34
.github/instructions/lowcode.instructions.md
vendored
34
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -588,7 +588,7 @@ Okuyan: `WizardDataSeeder` · Yazan: `ListFormWizardAppService` ve `WizardSeedSy
|
|||
"IsCreatedField": true, // tabloda CreatorId var mı (audit)
|
||||
"InsertedRecords": { // bu çalıştırmanın GERÇEKTEN yarattığı kayıtlar
|
||||
"LanguageKeys": [], "PermissionGroupNames": [], "PermissionNames": [],
|
||||
"MenuCodes": [], "DataSourceCodes": []
|
||||
"MenuCodes": [], "DataSourceCodes": [], "ModuleIds": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -606,8 +606,9 @@ Elle bu dosyayı yazarken `ListForm`/`Fields` bölümlerini uydurma — wizard'
|
|||
sunucu üretsin.
|
||||
|
||||
`InsertedRecords` **silme sözleşmesidir**: wizard silindiğinde yalnızca burada listelenen
|
||||
kayıtlar silinir, paylaşılan kayıtlara (var olan izin grubu, var olan üst menü) dokunulmaz.
|
||||
Elle dosya yazarken buraya var olan bir kaydı koyma — silme onu da götürür.
|
||||
kayıtlar silinmeye **aday** olur, önceden var olan kayıtlara (var olan izin grubu, var olan üst
|
||||
menü) dokunulmaz. Aday kayıt da yalnızca başka bir yerde kullanılmıyorsa silinir (§4.5 Silme).
|
||||
Elle dosya yazarken buraya var olan bir kaydı koyma — kullanılmıyorsa silme onu da götürür.
|
||||
|
||||
### 4.2 `Wizard` bloğu — alan alan
|
||||
|
||||
|
|
@ -1217,9 +1218,30 @@ yolu vardır:
|
|||
> `List` yolunda seeder ekranı uygularken `ListForm`, `ListFormField` ve `ListFormWorkflow`
|
||||
> kayıtlarını **silip yeniden yazar**; idempotency kapısını geçen dosya için bu güvenlidir.
|
||||
|
||||
**Silme:** yalnızca `InsertedRecords` içindekiler silinir. Paylaşılan üst menü ve izin grubu
|
||||
başka bir wizard tarafından yaratıldıysa kalır. Bu yüzden `InsertedRecords` listesine var olan
|
||||
bir kaydı yazmak veri kaybettirir.
|
||||
**Silme** (`WizardRecordCleaner`): ekran (`ListForm` + alanlar + iş akışı) ve `InsertedRecords`
|
||||
içindekiler, **başka bir yerde kullanılmıyorsa** silinir. Sıra bağımlılık yönündedir — önce
|
||||
kullanan, sonra kullanılan — ve her kontrol o ana kadar kalan kayıtlara karşı yapılır:
|
||||
|
||||
| Kayıt | Silinmez, çünkü kullanan var |
|
||||
| --- | --- |
|
||||
| ListForm (+ alanlar, iş akışı) | başka bir ekranın `SubFormsJson`'u |
|
||||
| Menü | altında menü kalmış (üst menü, son alt menüsü gidince silinir) |
|
||||
| Yetki | alt yetki, menü `RequiredPermissionName`, ekran/alan `PermissionJson`, intranet widget'ı |
|
||||
| Yetki ataması | — yetkiyle birlikte gider (tüm roller/kullanıcılar) |
|
||||
| Yetki grubu | içinde yetki kalmış |
|
||||
| Modül | menü `ModuleId`, yetkinin modül listesi, kiracının modül listesi |
|
||||
| Veri kaynağı | ekran, Custom Endpoint, zamanlanmış iş |
|
||||
| Dil anahtarı | menü/yetki/yetki grubu adı, ekran başlığı/açıklaması, alan başlığı |
|
||||
|
||||
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.
|
||||
Silme ucu (`DeleteFile`) her kaydın akıbetini (`Deleted` / `Kept` + kullananlar / `NotFound`)
|
||||
`WizardDeleteResultDto` olarak döner ve loglar; Wizard Manager bunu rapor olarak gösterir.
|
||||
Düzenleme yolu (`EditFileName`) aynı sınıfı `Redeploy` kipinde çağırır: üst menü, yetki grubu,
|
||||
modül, veri kaynağı ve admin dışındaki rollerin yetki atamaları korunur — hemen ardından yeniden
|
||||
kullanılırlar; silinip üretilseler menü sırası ve rol atamaları kaybolurdu. Wizard'ın yarattığı
|
||||
modül `InsertedRecords.ModuleIds` ile izlenir.
|
||||
|
||||
### 4.6 Alt formlar ve widget'lar
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public interface IListFormWizardAppService
|
|||
Task Create(ListFormWizardDto input);
|
||||
Task<List<WizardFileInfoDto>> GetFiles();
|
||||
Task<WizardSeedFileDto> GetFile(string fileName);
|
||||
Task DeleteFile(string fileName);
|
||||
Task<WizardDeleteResultDto> DeleteFile(string fileName);
|
||||
|
||||
/// <summary>Izin gruplarini iki dilli gorunen adlariyla dondurur.</summary>
|
||||
Task<List<WizardPermissionGroupDto>> GetPermissionGroups();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using Sozsoft.Platform.Enums;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
|
||||
|
|
@ -360,6 +361,39 @@ public class WizardInsertedRecordsDto
|
|||
|
||||
/// <summary>Bu Wizard tarafından oluşturulan yeni DataSource kodları.</summary>
|
||||
public List<string> DataSourceCodes { get; set; } = [];
|
||||
|
||||
/// <summary>Bu Wizard tarafından oluşturulan yeni Module kayıtları.</summary>
|
||||
public List<string> ModuleIds { get; set; } = [];
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasAny =>
|
||||
LanguageKeys.Count > 0 || PermissionGroupNames.Count > 0 || PermissionNames.Count > 0 ||
|
||||
MenuCodes.Count > 0 || DataSourceCodes.Count > 0 || ModuleIds.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>Kaydi kullanan (silinmesine engel olan) taraf.</summary>
|
||||
public class WizardRecordRefDto
|
||||
{
|
||||
public WizardRecordTypeEnum RecordType { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Wizard silinirken tek bir kaydin akibeti.</summary>
|
||||
public class WizardDeleteLogItemDto
|
||||
{
|
||||
public WizardRecordTypeEnum RecordType { get; set; }
|
||||
public string Name { get; set; }
|
||||
public WizardDeleteStatusEnum Status { get; set; }
|
||||
public WizardDeleteReasonEnum Reason { get; set; }
|
||||
public List<WizardRecordRefDto> UsedBy { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Wizard silme raporu; silinen, korunan ve bulunamayan her kayit tek tek listelenir.</summary>
|
||||
public class WizardDeleteResultDto
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public string WizardName { get; set; }
|
||||
public List<WizardDeleteLogItemDto> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Wizard seed dosyası özet bilgisi (yönetim listesi için).</summary>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ public class ListFormWizardAppService(
|
|||
IDynamicDataManager dynamicDataManager,
|
||||
IListFormSeedDataApplier seedDataApplier,
|
||||
ICrudEndpointSeedApplier crudEndpointSeedApplier,
|
||||
WizardRecordCleaner recordCleaner,
|
||||
ILogger<ListFormWizardAppService> logger
|
||||
) : PlatformAppService(), IListFormWizardAppService
|
||||
{
|
||||
|
|
@ -75,6 +76,7 @@ public class ListFormWizardAppService(
|
|||
private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager;
|
||||
private readonly IListFormSeedDataApplier _seedDataApplier = seedDataApplier;
|
||||
private readonly ICrudEndpointSeedApplier _crudEndpointSeedApplier = crudEndpointSeedApplier;
|
||||
private readonly WizardRecordCleaner _recordCleaner = recordCleaner;
|
||||
private readonly ILogger<ListFormWizardAppService> logger = logger;
|
||||
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ public class ListFormWizardAppService(
|
|||
{
|
||||
// Ilk deploy zamani wizard'in kimligidir; duzenleme onu degistirmez.
|
||||
input.SeededAt = await ReadSeededAtAsync(editFileName!);
|
||||
await DeleteFileInternalAsync(editFileName!);
|
||||
await DeleteFileInternalAsync(editFileName!, WizardDeleteMode.Redeploy);
|
||||
}
|
||||
|
||||
var wizardName = input.WizardName?.Trim();
|
||||
|
|
@ -252,7 +254,7 @@ public class ListFormWizardAppService(
|
|||
if (input.CreateMenu)
|
||||
{
|
||||
//Menu Parent
|
||||
await EnsureModuleAsync(input.MenuParentModuleId);
|
||||
await EnsureModuleAsync(input.MenuParentModuleId, inserted);
|
||||
|
||||
var menuQueryable = await repoMenu.GetQueryableAsync();
|
||||
var menuParent = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == input.MenuParentCode));
|
||||
|
|
@ -621,12 +623,7 @@ public class ListFormWizardAppService(
|
|||
MenuUrl = seed?.Wizard?.MenuUrl ?? string.Empty,
|
||||
CreatedAt = seed?.Wizard?.SeededAt.ToString("s") ?? string.Empty,
|
||||
MenuPath = seed?.Wizard != null ? ResolveMenuPath(seed.Wizard, menus) : [],
|
||||
HasInsertedRecords = seed?.InsertedRecords != null &&
|
||||
(seed.InsertedRecords.LanguageKeys.Count > 0 ||
|
||||
seed.InsertedRecords.PermissionGroupNames.Count > 0 ||
|
||||
seed.InsertedRecords.PermissionNames.Count > 0 ||
|
||||
seed.InsertedRecords.MenuCodes.Count > 0 ||
|
||||
seed.InsertedRecords.DataSourceCodes.Count > 0)
|
||||
HasInsertedRecords = seed?.InsertedRecords?.HasAny == true
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -779,28 +776,63 @@ public class ListFormWizardAppService(
|
|||
|
||||
[UnitOfWork]
|
||||
[Authorize(AppCodes.Listforms.Wizards.Delete)]
|
||||
public Task DeleteFile(string fileName)
|
||||
public async Task<WizardDeleteResultDto> DeleteFile(string fileName)
|
||||
{
|
||||
return DeleteFileInternalAsync(fileName);
|
||||
await EnsureNoDependentWizardsAsync(fileName);
|
||||
return await DeleteFileInternalAsync(fileName, WizardDeleteMode.Delete);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seed dosyasini ve wizard'in urettigi kayitlari siler. Yetki kontrolu cagiran uca aittir:
|
||||
/// silme yolunda <c>Delete</c>, duzenleme yolunda <c>Update</c> politikasi kullanilir.
|
||||
/// Bagli wizard'lari (alt formlar / component bagimliliklari) olan bir wizard silinemez; once
|
||||
/// bagli olanlar silinir. Kural listedeki gruplamayla ayni kaynaktandir
|
||||
/// (<see cref="FillWizardDependenciesAsync"/>); istemcideki devre disi buton yalnizca bir kolayliktir.
|
||||
/// </summary>
|
||||
private async Task DeleteFileInternalAsync(string fileName)
|
||||
private async Task EnsureNoDependentWizardsAsync(string fileName)
|
||||
{
|
||||
var files = await GetFiles();
|
||||
var file = files.FirstOrDefault(f => string.Equals(f.FileName, fileName, StringComparison.OrdinalIgnoreCase));
|
||||
if (file == null || file.Dependencies.Count == 0)
|
||||
return;
|
||||
|
||||
var names = files
|
||||
.Where(f => file.Dependencies.Contains(f.FileName, StringComparer.OrdinalIgnoreCase))
|
||||
.Select(f => f.WizardName ?? f.FileName);
|
||||
|
||||
throw new UserFriendlyException(L["App.ListForm.WizardDeleteBlockedByDependencies", string.Join(", ", names)]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seed dosyasini ve wizard'in urettigi, baska yerde kullanilmayan kayitlari siler; her kaydin
|
||||
/// akibeti rapora ve loga yazilir (<see cref="WizardRecordCleaner"/>). Yetki kontrolu cagiran
|
||||
/// uca aittir: silme yolunda <c>Delete</c>, duzenleme yolunda <c>Update</c> politikasi kullanilir.
|
||||
/// </summary>
|
||||
private async Task<WizardDeleteResultDto> DeleteFileInternalAsync(string fileName, WizardDeleteMode mode)
|
||||
{
|
||||
var filePath = ResolveWizardSeedFilePath(fileName);
|
||||
|
||||
var json = await File.ReadAllTextAsync(filePath);
|
||||
var seed = WizardSeedFileDto.FromSeedJson(json);
|
||||
|
||||
var result = new WizardDeleteResultDto
|
||||
{
|
||||
FileName = fileName,
|
||||
WizardName = seed?.Wizard?.WizardName ?? fileName,
|
||||
};
|
||||
|
||||
if (seed != null)
|
||||
await DeleteWizardDataAsync(seed);
|
||||
result.Items.AddRange(await _recordCleaner.CleanAsync(seed, mode));
|
||||
|
||||
File.Delete(filePath);
|
||||
result.Items.Add(new WizardDeleteLogItemDto
|
||||
{
|
||||
RecordType = WizardRecordTypeEnum.SeedFile,
|
||||
Name = $"{SeedPathResolver.WizardFolder}/{fileName}",
|
||||
Status = WizardDeleteStatusEnum.Deleted,
|
||||
});
|
||||
logger.LogInformation("Wizard seed file deleted ({Mode}): {FilePath}", mode, filePath);
|
||||
|
||||
await _languageTextAppService.ClearRedisCacheAsync();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -834,97 +866,6 @@ public class ListFormWizardAppService(
|
|||
return filePath;
|
||||
}
|
||||
|
||||
private async Task DeleteWizardDataAsync(WizardSeedFileDto seed)
|
||||
{
|
||||
var ins = seed.InsertedRecords;
|
||||
var listFormCode = seed.Wizard?.ListFormCode;
|
||||
var menuParentCode = seed.Wizard?.MenuParentCode;
|
||||
|
||||
// ListForm ve alanlarını sil. Custom yolunda wizard hiç ListForm üretmediği için
|
||||
// bu blok atlanır; aksi halde aynı kodu taşıyan bir List wizard'ının ListForm'u silinirdi.
|
||||
var isCustomComponent = seed.Wizard?.ComponentKind == WizardComponentKindEnum.Custom;
|
||||
if (!isCustomComponent && !string.IsNullOrWhiteSpace(listFormCode))
|
||||
{
|
||||
var lf = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
||||
if (lf != null) await repoListForm.DeleteAsync(lf, autoSave: true);
|
||||
|
||||
var fields = await repoListFormField.GetListAsync(a => a.ListFormCode == listFormCode);
|
||||
if (fields.Count > 0) await repoListFormField.DeleteManyAsync(fields, autoSave: true);
|
||||
}
|
||||
|
||||
if (ins == null) return;
|
||||
|
||||
// Permission grants
|
||||
if (ins.PermissionNames.Count > 0)
|
||||
{
|
||||
var grants = await permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName);
|
||||
var toDelete = grants.Where(g => ins.PermissionNames.Contains(g.Name)).ToList();
|
||||
foreach (var g in toDelete) await permissionGrantRepository.DeleteAsync(g);
|
||||
}
|
||||
|
||||
// Permissions
|
||||
foreach (var name in ins.PermissionNames)
|
||||
{
|
||||
var p = await repoPerm.FirstOrDefaultAsync(a => a.Name == name);
|
||||
if (p != null) await repoPerm.DeleteAsync(p, autoSave: true);
|
||||
}
|
||||
|
||||
// Permission Groups
|
||||
foreach (var name in ins.PermissionGroupNames)
|
||||
{
|
||||
// Permission grubu MenuParentCode ile aynıysa diğer Wizard'lar tarafından
|
||||
// paylaşılabilir. Parent menü gibi bu grubu da Wizard silme yaşam
|
||||
// döngüsünün dışında tut.
|
||||
if (string.Equals(name, menuParentCode, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
// Aynı grupta başka Wizard'lara ait permission kayıtları kaldıysa grup
|
||||
// halen kullanımdadır ve silinmemelidir.
|
||||
if (await repoPerm.AnyAsync(a => a.GroupName == name))
|
||||
continue;
|
||||
|
||||
var pg = await repoPermGroup.FirstOrDefaultAsync(a => a.Name == name);
|
||||
if (pg != null) await repoPermGroup.DeleteAsync(pg, autoSave: true);
|
||||
}
|
||||
|
||||
// Menus
|
||||
foreach (var code in ins.MenuCodes)
|
||||
{
|
||||
// Parent menüler birden fazla Wizard tarafından paylaşılabilir ve Wizard
|
||||
// silme yaşam döngüsüne ait değildir. Altında başka bir Wizard olmasa bile
|
||||
// parent menüyü koru.
|
||||
if (string.Equals(code, menuParentCode, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var m = await repoMenu.FirstOrDefaultAsync(a => a.Code == code);
|
||||
if (m != null) await repoMenu.DeleteAsync(m, autoSave: true);
|
||||
}
|
||||
|
||||
// DataSources
|
||||
foreach (var code in ins.DataSourceCodes)
|
||||
{
|
||||
var ds = await repoDataSource.FirstOrDefaultAsync(a => a.Code == code);
|
||||
if (ds != null) await repoDataSource.DeleteAsync(ds, autoSave: true);
|
||||
}
|
||||
|
||||
// Language Keys ve Texts
|
||||
var appName = PlatformConsts.AppName;
|
||||
foreach (var key in ins.LanguageKeys)
|
||||
{
|
||||
// Korunan parent menünün görünen adını da kullanılabilir halde tut.
|
||||
if (string.Equals(key, menuParentCode, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var lk = await repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == appName && a.Key == key);
|
||||
if (lk == null) continue;
|
||||
|
||||
var texts = await repoLangText.GetListAsync(a => a.ResourceName == appName && a.Key == key);
|
||||
if (texts.Count > 0) await repoLangText.DeleteManyAsync(texts, autoSave: true);
|
||||
await repoLangKey.DeleteAsync(lk, autoSave: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Wizard adi hem menu/permission/dil anahtarlarinin hem de seed dosyasi adinin kaynagidir;
|
||||
/// bu yuzden yalnizca ASCII harf/rakam/alt cizgi kabul edilir. Bosluk ve Turkce karakter
|
||||
|
|
@ -2367,7 +2308,7 @@ public class ListFormWizardAppService(
|
|||
/// Wizard'da secilen modul kaydi yoksa olusturur; menunun ModuleId yabanci anahtari
|
||||
/// tanimsiz bir module bagli kalmasin diye menu yazilmadan once cagrilir.
|
||||
/// </summary>
|
||||
private async Task EnsureModuleAsync(string moduleId)
|
||||
private async Task EnsureModuleAsync(string moduleId, WizardInsertedRecordsDto inserted)
|
||||
{
|
||||
var cleaned = moduleId?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(cleaned))
|
||||
|
|
@ -2381,6 +2322,7 @@ public class ListFormWizardAppService(
|
|||
}
|
||||
|
||||
await repoModule.InsertAsync(new Module(cleaned) { Name = cleaned }, autoSave: false);
|
||||
inserted.ModuleIds.Add(cleaned);
|
||||
logger.LogInformation("Module '{ModuleId}' wizard tarafindan olusturuldu.", cleaned);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,485 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sozsoft.Languages.Entities;
|
||||
using Sozsoft.Platform.Entities;
|
||||
using Sozsoft.Platform.Enums;
|
||||
using Sozsoft.Platform.Extensions;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
using Volo.Abp.PermissionManagement;
|
||||
using Volo.Abp.TenantManagement;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
/// <summary>
|
||||
/// Bir wizard'in deploy sirasinda urettigi kayitlari siler ve her kaydin akibetini raporlar.
|
||||
/// <para>
|
||||
/// Kural: wizard'in urettigi bir kayit, <b>baska bir yerde kullanilmiyorsa</b> silinir. Kullanim
|
||||
/// kontrolu silme sirasinin ilerleyen adimlarinda kalan kayitlara karsi yapilir; bu yuzden sira
|
||||
/// bagimlilik yonundedir — once kullanan (ListForm, menu), sonra kullanilan (yetki, grup, modul,
|
||||
/// veri kaynagi, dil anahtari). Kendi ekraninin kullandigi kayit ekranla birlikte gider; baska bir
|
||||
/// ekranin kullandigi kayit birakilir ve kullananlar rapora yazilir.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="WizardDeleteMode.Redeploy"/> duzenleme yoludur: kayitlar hemen ardindan ayni
|
||||
/// cevaplardan yeniden uretilir. Bu yolda paylasilan kaplar (ust menu, izin grubu, modul, veri
|
||||
/// kaynagi) ve diger rollerin yetki atamalari korunur; silinip yeniden uretilseler menu sirasi ve
|
||||
/// rol atamalari kaybolurdu.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class WizardRecordCleaner(
|
||||
IRepository<ListForm, Guid> repoListForm,
|
||||
IRepository<ListFormField, Guid> repoListFormField,
|
||||
IRepository<ListFormWorkflow, string> repoListFormWorkflow,
|
||||
IRepository<DataSource, Guid> repoDataSource,
|
||||
IRepository<LanguageKey, Guid> repoLangKey,
|
||||
IRepository<LanguageText, Guid> repoLangText,
|
||||
IRepository<PermissionDefinitionRecord, Guid> repoPerm,
|
||||
IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup,
|
||||
IRepository<PermissionGrant, Guid> repoPermGrant,
|
||||
IRepository<Menu, Guid> repoMenu,
|
||||
IRepository<Module, string> repoModule,
|
||||
IRepository<CustomEndpoint, Guid> repoCustomEndpoint,
|
||||
IRepository<BackgroundWorker, Guid> repoBackgroundWorker,
|
||||
IRepository<IntranetWidget, Guid> repoIntranetWidget,
|
||||
ITenantRepository tenantRepository,
|
||||
ILogger<WizardRecordCleaner> logger) : ITransientDependency
|
||||
{
|
||||
/// <summary>Raporda bir kaydi kullananlarin en fazla bu kadari listelenir.</summary>
|
||||
private const int MaxUsedBy = 20;
|
||||
|
||||
public async Task<List<WizardDeleteLogItemDto>> CleanAsync(WizardSeedFileDto seed, WizardDeleteMode mode)
|
||||
{
|
||||
var report = new List<WizardDeleteLogItemDto>();
|
||||
var wizard = seed.Wizard;
|
||||
var ins = seed.InsertedRecords ?? new WizardInsertedRecordsDto();
|
||||
|
||||
// Custom yolunda ListForm uretilmez; ayni kodu tasiyan bir List wizard'inin ekrani silinmesin.
|
||||
if (wizard?.ComponentKind != WizardComponentKindEnum.Custom && !string.IsNullOrWhiteSpace(wizard?.ListFormCode))
|
||||
await CleanListFormAsync(wizard.ListFormCode, mode, report);
|
||||
|
||||
await CleanMenusAsync(ins.MenuCodes, wizard?.MenuParentCode, mode, report);
|
||||
await CleanPermissionsAsync(ins.PermissionNames, mode, report);
|
||||
await CleanPermissionGroupsAsync(ins.PermissionGroupNames, mode, report);
|
||||
await CleanModulesAsync(ins.ModuleIds, mode, report);
|
||||
await CleanDataSourcesAsync(ins.DataSourceCodes, mode, report);
|
||||
await CleanLanguageKeysAsync(ins.LanguageKeys, report);
|
||||
|
||||
foreach (var item in report)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Wizard '{WizardName}' {Mode}: {RecordType} '{Name}' -> {Status} {Reason} {UsedBy}",
|
||||
wizard?.WizardName, mode, item.RecordType, item.Name, item.Status, item.Reason,
|
||||
string.Join(", ", item.UsedBy.Select(u => $"{u.RecordType}:{u.Name}")));
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
// ── ListForm ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Ekran baska bir ekranin alt formuysa silinmez; alanlari ve is akisi da ekranla birlikte
|
||||
/// kalir, aksi halde ust ekranin sekmesi bozulurdu. Duzenlemede deploy ekrani zaten yeniden
|
||||
/// kurar, bu kontrol yapilmaz.
|
||||
/// </summary>
|
||||
private async Task CleanListFormAsync(string listFormCode, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
||||
{
|
||||
var listForm = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
||||
var fields = await repoListFormField.GetListAsync(a => a.ListFormCode == listFormCode);
|
||||
var criteria = await repoListFormWorkflow.GetListAsync(a => a.ListFormCode == listFormCode);
|
||||
|
||||
if (listForm == null)
|
||||
{
|
||||
Add(report, WizardRecordTypeEnum.ListForm, listFormCode, WizardDeleteStatusEnum.NotFound);
|
||||
}
|
||||
else if (mode == WizardDeleteMode.Delete)
|
||||
{
|
||||
var quoted = Quote(listFormCode);
|
||||
var parents = await repoListForm.GetListAsync(a =>
|
||||
a.ListFormCode != listFormCode && a.SubFormsJson != null && a.SubFormsJson.Contains(quoted));
|
||||
|
||||
if (parents.Count > 0)
|
||||
{
|
||||
var usedBy = parents.Select(p => Ref(WizardRecordTypeEnum.ListForm, p.ListFormCode)).ToList();
|
||||
AddKept(report, WizardRecordTypeEnum.ListForm, listFormCode, usedBy);
|
||||
fields.ForEach(f => AddKept(report, WizardRecordTypeEnum.ListFormField, FieldName(f), usedBy));
|
||||
criteria.ForEach(c => AddKept(report, WizardRecordTypeEnum.Workflow, CriteriaName(c), usedBy));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Alanlar ve is akisi ListForm'a zorunlu (cascade) anahtarla baglidir: once ekran silinirse
|
||||
// EF izlenen bagimlilari da siler ve ardindan gelen toplu silme 0 satir etkileyip
|
||||
// concurrency hatasi firlatir. Bu yuzden bagimlilar ekrandan once silinir.
|
||||
if (fields.Count > 0)
|
||||
{
|
||||
await repoListFormField.DeleteManyAsync(fields, autoSave: true);
|
||||
fields.ForEach(f => Add(report, WizardRecordTypeEnum.ListFormField, FieldName(f), WizardDeleteStatusEnum.Deleted));
|
||||
}
|
||||
|
||||
if (criteria.Count > 0)
|
||||
{
|
||||
await repoListFormWorkflow.DeleteManyAsync(criteria, autoSave: true);
|
||||
criteria.ForEach(c => Add(report, WizardRecordTypeEnum.Workflow, CriteriaName(c), WizardDeleteStatusEnum.Deleted));
|
||||
}
|
||||
|
||||
if (listForm != null)
|
||||
{
|
||||
await repoListForm.DeleteAsync(listForm, autoSave: true);
|
||||
Add(report, WizardRecordTypeEnum.ListForm, listFormCode, WizardDeleteStatusEnum.Deleted);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FieldName(ListFormField field) => $"{field.ListFormCode}.{field.FieldName}";
|
||||
|
||||
private static string CriteriaName(ListFormWorkflow criteria) =>
|
||||
string.IsNullOrWhiteSpace(criteria.Title) ? criteria.Id : $"{criteria.Title} ({criteria.Id})";
|
||||
|
||||
// ── Menu ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Altinda baska menu kalan menu silinmez. Wizard'in kendi menusu ust menuden once silinir;
|
||||
/// ust menunun bos kalip kalmadigi ancak o zaman bilinir.
|
||||
/// </summary>
|
||||
private async Task CleanMenusAsync(List<string> codes, string? menuParentCode, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
||||
{
|
||||
var pending = new List<string>();
|
||||
foreach (var code in codes.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (mode == WizardDeleteMode.Redeploy && string.Equals(code, menuParentCode, StringComparison.OrdinalIgnoreCase))
|
||||
AddRedeploy(report, WizardRecordTypeEnum.Menu, code);
|
||||
else
|
||||
pending.Add(code);
|
||||
}
|
||||
|
||||
// Ust menu, alt menusu silindikten sonra bosalir; ilerleme durana kadar tekrar denenir.
|
||||
var progressed = true;
|
||||
while (pending.Count > 0 && progressed)
|
||||
{
|
||||
progressed = false;
|
||||
foreach (var code in pending.ToList())
|
||||
{
|
||||
var menu = await repoMenu.FirstOrDefaultAsync(a => a.Code == code);
|
||||
if (menu == null)
|
||||
{
|
||||
Add(report, WizardRecordTypeEnum.Menu, code, WizardDeleteStatusEnum.NotFound);
|
||||
pending.Remove(code);
|
||||
progressed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await repoMenu.AnyAsync(a => a.ParentCode == code))
|
||||
continue;
|
||||
|
||||
await repoMenu.DeleteAsync(menu, autoSave: true);
|
||||
Add(report, WizardRecordTypeEnum.Menu, code, WizardDeleteStatusEnum.Deleted);
|
||||
pending.Remove(code);
|
||||
progressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var code in pending)
|
||||
{
|
||||
var children = await repoMenu.GetListAsync(a => a.ParentCode == code);
|
||||
AddKept(report, WizardRecordTypeEnum.Menu, code,
|
||||
[.. children.Select(c => Ref(WizardRecordTypeEnum.Menu, c.Code))]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Yetki ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Kalan bir menu, ekran, alan, widget ya da alt yetki bir yetkiyi kullaniyorsa yetki silinmez.
|
||||
/// Alt yetkiler ust yetkiden once silinir. Silinen yetkinin atamalari da silinir: silme
|
||||
/// yolunda tum saglayicilarinki, duzenlemede yalnizca admin rolununki — diger rollerin
|
||||
/// atamalari yeniden uretilen ayni adli yetkiye bagli kalir.
|
||||
/// </summary>
|
||||
private async Task CleanPermissionsAsync(List<string> names, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
||||
{
|
||||
var pending = names.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var blockers = new Dictionary<string, List<WizardRecordRefDto>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var progressed = true;
|
||||
while (pending.Count > 0 && progressed)
|
||||
{
|
||||
progressed = false;
|
||||
foreach (var name in pending.ToList())
|
||||
{
|
||||
var permission = await repoPerm.FirstOrDefaultAsync(a => a.Name == name);
|
||||
if (permission == null)
|
||||
{
|
||||
Add(report, WizardRecordTypeEnum.Permission, name, WizardDeleteStatusEnum.NotFound);
|
||||
pending.Remove(name);
|
||||
progressed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
var usedBy = await FindPermissionUsagesAsync(name);
|
||||
if (usedBy.Count > 0)
|
||||
{
|
||||
blockers[name] = usedBy;
|
||||
continue;
|
||||
}
|
||||
|
||||
await DeletePermissionGrantsAsync(name, mode, report);
|
||||
await repoPerm.DeleteAsync(permission, autoSave: true);
|
||||
Add(report, WizardRecordTypeEnum.Permission, name, WizardDeleteStatusEnum.Deleted);
|
||||
pending.Remove(name);
|
||||
blockers.Remove(name);
|
||||
progressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var name in pending)
|
||||
AddKept(report, WizardRecordTypeEnum.Permission, name, blockers.GetValueOrDefault(name) ?? []);
|
||||
}
|
||||
|
||||
private async Task<List<WizardRecordRefDto>> FindPermissionUsagesAsync(string name)
|
||||
{
|
||||
var quoted = Quote(name);
|
||||
var usedBy = new List<WizardRecordRefDto>();
|
||||
|
||||
usedBy.AddRange((await repoPerm.GetListAsync(a => a.ParentName == name))
|
||||
.Select(p => Ref(WizardRecordTypeEnum.Permission, p.Name)));
|
||||
usedBy.AddRange((await repoMenu.GetListAsync(a => a.RequiredPermissionName == name))
|
||||
.Select(m => Ref(WizardRecordTypeEnum.Menu, m.Code)));
|
||||
usedBy.AddRange((await repoListForm.GetListAsync(a => a.PermissionJson != null && a.PermissionJson.Contains(quoted)))
|
||||
.Select(l => Ref(WizardRecordTypeEnum.ListForm, l.ListFormCode)));
|
||||
usedBy.AddRange((await repoListFormField.GetListAsync(a => a.PermissionJson != null && a.PermissionJson.Contains(quoted)))
|
||||
.Select(f => Ref(WizardRecordTypeEnum.ListFormField, FieldName(f))));
|
||||
usedBy.AddRange((await repoIntranetWidget.GetListAsync(a => a.PermissionName == name))
|
||||
.Select(w => Ref(WizardRecordTypeEnum.IntranetWidget, w.Name)));
|
||||
|
||||
return usedBy;
|
||||
}
|
||||
|
||||
private async Task DeletePermissionGrantsAsync(string name, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
||||
{
|
||||
var grants = mode == WizardDeleteMode.Delete
|
||||
? await repoPermGrant.GetListAsync(a => a.Name == name)
|
||||
: await repoPermGrant.GetListAsync(a =>
|
||||
a.Name == name && a.ProviderName == "R" && a.ProviderKey == PlatformConsts.AbpIdentity.User.AdminRoleName);
|
||||
|
||||
if (grants.Count == 0)
|
||||
return;
|
||||
|
||||
await repoPermGrant.DeleteManyAsync(grants, autoSave: true);
|
||||
foreach (var grant in grants)
|
||||
Add(report, WizardRecordTypeEnum.PermissionGrant, $"{grant.Name} → {grant.ProviderName}:{grant.ProviderKey}", WizardDeleteStatusEnum.Deleted);
|
||||
}
|
||||
|
||||
// ── Yetki grubu, modul, veri kaynagi ───────────────────────────────────────────
|
||||
|
||||
private async Task CleanPermissionGroupsAsync(List<string> names, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
||||
{
|
||||
foreach (var name in names.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (mode == WizardDeleteMode.Redeploy)
|
||||
{
|
||||
AddRedeploy(report, WizardRecordTypeEnum.PermissionGroup, name);
|
||||
continue;
|
||||
}
|
||||
|
||||
var group = await repoPermGroup.FirstOrDefaultAsync(a => a.Name == name);
|
||||
if (group == null)
|
||||
{
|
||||
Add(report, WizardRecordTypeEnum.PermissionGroup, name, WizardDeleteStatusEnum.NotFound);
|
||||
continue;
|
||||
}
|
||||
|
||||
var permissions = await repoPerm.GetListAsync(a => a.GroupName == name);
|
||||
if (permissions.Count > 0)
|
||||
{
|
||||
AddKept(report, WizardRecordTypeEnum.PermissionGroup, name,
|
||||
[.. permissions.Select(p => Ref(WizardRecordTypeEnum.Permission, p.Name))]);
|
||||
continue;
|
||||
}
|
||||
|
||||
await repoPermGroup.DeleteAsync(group, autoSave: true);
|
||||
Add(report, WizardRecordTypeEnum.PermissionGroup, name, WizardDeleteStatusEnum.Deleted);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Modul; menuler, yetkilerin modul listesi ve kiracilarin modul listesi tarafindan kullanilir.</summary>
|
||||
private async Task CleanModulesAsync(List<string> ids, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
||||
{
|
||||
if (ids.Count == 0)
|
||||
return;
|
||||
|
||||
List<PermissionDefinitionRecord>? permissions = null;
|
||||
List<Volo.Abp.TenantManagement.Tenant>? tenants = null;
|
||||
|
||||
foreach (var id in ids.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (mode == WizardDeleteMode.Redeploy)
|
||||
{
|
||||
AddRedeploy(report, WizardRecordTypeEnum.Module, id);
|
||||
continue;
|
||||
}
|
||||
|
||||
var module = await repoModule.FindAsync(id);
|
||||
if (module == null)
|
||||
{
|
||||
Add(report, WizardRecordTypeEnum.Module, id, WizardDeleteStatusEnum.NotFound);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Modul listeleri ekstra ozellikte metin olarak durur; sorguyla suzulemez, bellekte bakilir.
|
||||
permissions ??= await repoPerm.GetListAsync();
|
||||
tenants ??= await tenantRepository.GetListAsync();
|
||||
|
||||
var usedBy = new List<WizardRecordRefDto>();
|
||||
usedBy.AddRange((await repoMenu.GetListAsync(a => a.ModuleId == id))
|
||||
.Select(m => Ref(WizardRecordTypeEnum.Menu, m.Code)));
|
||||
usedBy.AddRange(permissions
|
||||
.Where(p => p.GetModules().Contains(id, StringComparer.OrdinalIgnoreCase))
|
||||
.Select(p => Ref(WizardRecordTypeEnum.Permission, p.Name)));
|
||||
usedBy.AddRange(tenants
|
||||
.Where(t => t.GetModules().Contains(id, StringComparer.OrdinalIgnoreCase))
|
||||
.Select(t => Ref(WizardRecordTypeEnum.Tenant, t.Name)));
|
||||
|
||||
if (usedBy.Count > 0)
|
||||
{
|
||||
AddKept(report, WizardRecordTypeEnum.Module, id, usedBy);
|
||||
continue;
|
||||
}
|
||||
|
||||
await repoModule.DeleteAsync(module, autoSave: true);
|
||||
Add(report, WizardRecordTypeEnum.Module, id, WizardDeleteStatusEnum.Deleted);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanDataSourcesAsync(List<string> codes, WizardDeleteMode mode, List<WizardDeleteLogItemDto> report)
|
||||
{
|
||||
foreach (var code in codes.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (mode == WizardDeleteMode.Redeploy)
|
||||
{
|
||||
AddRedeploy(report, WizardRecordTypeEnum.DataSource, code);
|
||||
continue;
|
||||
}
|
||||
|
||||
var dataSource = await repoDataSource.FirstOrDefaultAsync(a => a.Code == code);
|
||||
if (dataSource == null)
|
||||
{
|
||||
Add(report, WizardRecordTypeEnum.DataSource, code, WizardDeleteStatusEnum.NotFound);
|
||||
continue;
|
||||
}
|
||||
|
||||
var usedBy = new List<WizardRecordRefDto>();
|
||||
usedBy.AddRange((await repoListForm.GetListAsync(a => a.DataSourceCode == code))
|
||||
.Select(l => Ref(WizardRecordTypeEnum.ListForm, l.ListFormCode)));
|
||||
usedBy.AddRange((await repoCustomEndpoint.GetListAsync(a => a.DataSourceCode == code))
|
||||
.Select(e => Ref(WizardRecordTypeEnum.CustomEndpoint, e.Name)));
|
||||
usedBy.AddRange((await repoBackgroundWorker.GetListAsync(a => a.DataSourceCode == code))
|
||||
.Select(w => Ref(WizardRecordTypeEnum.BackgroundWorker, w.Name)));
|
||||
|
||||
if (usedBy.Count > 0)
|
||||
{
|
||||
AddKept(report, WizardRecordTypeEnum.DataSource, code, usedBy);
|
||||
continue;
|
||||
}
|
||||
|
||||
await repoDataSource.DeleteAsync(dataSource, autoSave: true);
|
||||
Add(report, WizardRecordTypeEnum.DataSource, code, WizardDeleteStatusEnum.Deleted);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dil anahtari ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Dil anahtari en son silinir; kalan menu, yetki, yetki grubu, ekran basligi ya da alan
|
||||
/// basligi onu gosteriyorsa birakilir. Alan basliklari (<c>App.Listform.ListformField.*</c>)
|
||||
/// ekranlar arasinda ortak oldugu icin bu kontrol olmadan baska ekranlarin basliklari kaybolurdu.
|
||||
/// </summary>
|
||||
private async Task CleanLanguageKeysAsync(List<string> keys, List<WizardDeleteLogItemDto> report)
|
||||
{
|
||||
var resourceName = PlatformConsts.AppName;
|
||||
|
||||
foreach (var key in keys.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var languageKey = await repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == resourceName && a.Key == key);
|
||||
if (languageKey == null)
|
||||
{
|
||||
Add(report, WizardRecordTypeEnum.LanguageKey, key, WizardDeleteStatusEnum.NotFound);
|
||||
continue;
|
||||
}
|
||||
|
||||
var usedBy = new List<WizardRecordRefDto>();
|
||||
usedBy.AddRange((await repoMenu.GetListAsync(a => a.DisplayName == key))
|
||||
.Select(m => Ref(WizardRecordTypeEnum.Menu, m.Code)));
|
||||
usedBy.AddRange((await repoPerm.GetListAsync(a => a.DisplayName == key))
|
||||
.Select(p => Ref(WizardRecordTypeEnum.Permission, p.Name)));
|
||||
usedBy.AddRange((await repoPermGroup.GetListAsync(a => a.DisplayName == key))
|
||||
.Select(g => Ref(WizardRecordTypeEnum.PermissionGroup, g.Name)));
|
||||
usedBy.AddRange((await repoListForm.GetListAsync(a => a.Title == key || a.Description == key))
|
||||
.Select(l => Ref(WizardRecordTypeEnum.ListForm, l.ListFormCode)));
|
||||
usedBy.AddRange((await repoListFormField.GetListAsync(a => a.CaptionName == key))
|
||||
.Select(f => Ref(WizardRecordTypeEnum.ListFormField, FieldName(f))));
|
||||
|
||||
if (usedBy.Count > 0)
|
||||
{
|
||||
AddKept(report, WizardRecordTypeEnum.LanguageKey, key, usedBy);
|
||||
continue;
|
||||
}
|
||||
|
||||
var texts = await repoLangText.GetListAsync(a => a.ResourceName == resourceName && a.Key == key);
|
||||
if (texts.Count > 0)
|
||||
await repoLangText.DeleteManyAsync(texts, autoSave: true);
|
||||
|
||||
await repoLangKey.DeleteAsync(languageKey, autoSave: true);
|
||||
Add(report, WizardRecordTypeEnum.LanguageKey, key, WizardDeleteStatusEnum.Deleted);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rapor ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>JSON icinde tirnakli deger olarak arar; kod bir baska kodun parcasiysa eslesmez.</summary>
|
||||
private static string Quote(string value) => $"\"{value}\"";
|
||||
|
||||
private static WizardRecordRefDto Ref(WizardRecordTypeEnum type, string name) => new() { RecordType = type, Name = name };
|
||||
|
||||
private static void Add(List<WizardDeleteLogItemDto> report, WizardRecordTypeEnum type, string name, WizardDeleteStatusEnum status)
|
||||
{
|
||||
report.Add(new WizardDeleteLogItemDto { RecordType = type, Name = name, Status = status });
|
||||
}
|
||||
|
||||
private static void AddKept(List<WizardDeleteLogItemDto> report, WizardRecordTypeEnum type, string name, List<WizardRecordRefDto> usedBy)
|
||||
{
|
||||
report.Add(new WizardDeleteLogItemDto
|
||||
{
|
||||
RecordType = type,
|
||||
Name = name,
|
||||
Status = WizardDeleteStatusEnum.Kept,
|
||||
Reason = WizardDeleteReasonEnum.InUse,
|
||||
UsedBy = [.. usedBy
|
||||
.DistinctBy(u => (u.RecordType, u.Name?.ToLowerInvariant()))
|
||||
.Take(MaxUsedBy)],
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddRedeploy(List<WizardDeleteLogItemDto> report, WizardRecordTypeEnum type, string name)
|
||||
{
|
||||
report.Add(new WizardDeleteLogItemDto
|
||||
{
|
||||
RecordType = type,
|
||||
Name = name,
|
||||
Status = WizardDeleteStatusEnum.Kept,
|
||||
Reason = WizardDeleteReasonEnum.Redeploy,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Wizard kayitlarinin hangi amacla silindigi.</summary>
|
||||
public enum WizardDeleteMode
|
||||
{
|
||||
/// <summary>Wizard Manager'dan silme: kullanilmayan her kayit gider.</summary>
|
||||
Delete,
|
||||
|
||||
/// <summary>Duzenleme: kayitlar hemen ardindan yeniden uretilir, paylasilan kaplar korunur.</summary>
|
||||
Redeploy,
|
||||
}
|
||||
|
|
@ -16362,6 +16362,156 @@
|
|||
"en": "Wizard file deleted successfully.",
|
||||
"tr": "Wizard dosyası başarıyla silindi."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardDeleteReport",
|
||||
"en": "Wizard Deletion Report",
|
||||
"tr": "Wizard Silme Raporu"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardDeleteSummary",
|
||||
"en": "Deleted: {0} · Kept: {1} · Not found: {2}",
|
||||
"tr": "Silinen: {0} · Korunan: {1} · Bulunamayan: {2}"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardDeleteUsedBy",
|
||||
"en": "Used by",
|
||||
"tr": "Kullanan"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardDeleteBlockedByDependencies",
|
||||
"en": "This wizard cannot be deleted because it has dependent wizards. Delete them first: {0}",
|
||||
"tr": "Bu wizard'a bağlı wizard'lar olduğu için silinemez. Önce bunları silin: {0}"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardDeleteRecordType",
|
||||
"en": "Record Type",
|
||||
"tr": "Kayıt Türü"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardDeleteStatus.Deleted",
|
||||
"en": "Deleted",
|
||||
"tr": "Silindi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardDeleteStatus.Kept",
|
||||
"en": "Kept",
|
||||
"tr": "Korundu"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardDeleteStatus.NotFound",
|
||||
"en": "Not found",
|
||||
"tr": "Bulunamadı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardDeleteReason.InUse",
|
||||
"en": "Still used elsewhere",
|
||||
"tr": "Başka bir yerde kullanılıyor"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardDeleteReason.Redeploy",
|
||||
"en": "Kept for redeploy",
|
||||
"tr": "Yeniden deploy için korundu"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.SeedFile",
|
||||
"en": "Seed File",
|
||||
"tr": "Seed Dosyası"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.ListForm",
|
||||
"en": "List Form",
|
||||
"tr": "Liste Formu"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.ListFormField",
|
||||
"en": "List Form Field",
|
||||
"tr": "Liste Form Alanı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.Workflow",
|
||||
"en": "Workflow Step",
|
||||
"tr": "İş Akışı Adımı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.PermissionGrant",
|
||||
"en": "Permission Grant",
|
||||
"tr": "Yetki Ataması"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.Permission",
|
||||
"en": "Permission",
|
||||
"tr": "Yetki"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.PermissionGroup",
|
||||
"en": "Permission Group",
|
||||
"tr": "Yetki Grubu"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.Menu",
|
||||
"en": "Menu",
|
||||
"tr": "Menü"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.Module",
|
||||
"en": "Module",
|
||||
"tr": "Modül"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.DataSource",
|
||||
"en": "Data Source",
|
||||
"tr": "Veri Kaynağı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.LanguageKey",
|
||||
"en": "Language Key",
|
||||
"tr": "Dil Anahtarı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.CustomEndpoint",
|
||||
"en": "Custom Endpoint",
|
||||
"tr": "Custom Endpoint"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.BackgroundWorker",
|
||||
"en": "Background Worker",
|
||||
"tr": "Zamanlanmış İş"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.IntranetWidget",
|
||||
"en": "Intranet Widget",
|
||||
"tr": "Intranet Widget"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardRecordType.Tenant",
|
||||
"en": "Tenant",
|
||||
"tr": "Kiracı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ListForm.WizardFileExport",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
namespace Sozsoft.Platform.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Wizard silme raporundaki kayit turleri. Ilk grup wizard'in urettigi kayitlardir; ikinci grup
|
||||
/// yalnizca bir kaydin neden korundugunu gostermek icin (kullanan taraf olarak) raporlanir.
|
||||
/// Istemci her degeri <c>App.ListForm.WizardRecordType.{Ad}</c> dil anahtariyla gosterir.
|
||||
/// </summary>
|
||||
public enum WizardRecordTypeEnum
|
||||
{
|
||||
SeedFile = 0,
|
||||
ListForm = 1,
|
||||
ListFormField = 2,
|
||||
Workflow = 3,
|
||||
PermissionGrant = 4,
|
||||
Permission = 5,
|
||||
PermissionGroup = 6,
|
||||
Menu = 7,
|
||||
Module = 8,
|
||||
DataSource = 9,
|
||||
LanguageKey = 10,
|
||||
|
||||
CustomEndpoint = 20,
|
||||
BackgroundWorker = 21,
|
||||
IntranetWidget = 22,
|
||||
Tenant = 23,
|
||||
}
|
||||
|
||||
/// <summary>Silme raporunda bir kaydin akibeti.</summary>
|
||||
public enum WizardDeleteStatusEnum
|
||||
{
|
||||
Deleted = 0,
|
||||
|
||||
/// <summary>Kayit baska bir yerde kullanildigi ya da yeniden deploy edilecegi icin birakildi.</summary>
|
||||
Kept = 1,
|
||||
|
||||
/// <summary>Seed dosyasinda izi var ama veritabaninda yok; daha once elle silinmis olabilir.</summary>
|
||||
NotFound = 2,
|
||||
}
|
||||
|
||||
/// <summary>Bir kaydin neden korundugu.</summary>
|
||||
public enum WizardDeleteReasonEnum
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>Baska bir kayit bu kayda baglidir; kullananlar raporda listelenir.</summary>
|
||||
InUse = 1,
|
||||
|
||||
/// <summary>Duzenleme yolunda paylasilan kayit korunur; deploy onu yeniden kullanir.</summary>
|
||||
Redeploy = 2,
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"EntityName": "Sal_D_Appoval",
|
||||
"GeneratedAt": "2026-09-02T08:17:19.9301653Z",
|
||||
"Endpoints": [
|
||||
{
|
||||
"Method": "POST",
|
||||
"Path": "/api/app/crudendpoint/Sal_D_Appoval",
|
||||
"OperationType": "Create",
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Method": "DELETE",
|
||||
"Path": "/api/app/crudendpoint/Sal_D_Appoval/{id}",
|
||||
"OperationType": "Delete",
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Method": "GET",
|
||||
"Path": "/api/app/crudendpoint/Sal_D_Appoval/{id}",
|
||||
"OperationType": "GetById",
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Method": "GET",
|
||||
"Path": "/api/app/crudendpoint/Sal_D_Appoval",
|
||||
"OperationType": "GetList",
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Method": "PUT",
|
||||
"Path": "/api/app/crudendpoint/Sal_D_Appoval/{id}",
|
||||
"OperationType": "Update",
|
||||
"IsActive": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
-- Create Table
|
||||
IF OBJECT_ID(N'[dbo].[Prj_T_Customer]', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[Prj_T_Customer] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT [PK_Prj_T_Customer] PRIMARY KEY,
|
||||
[TenantId] UNIQUEIDENTIFIER NULL,
|
||||
[Name] NVARCHAR(300) NOT NULL,
|
||||
[ContactName] NVARCHAR(200) NULL,
|
||||
[Email] NVARCHAR(200) NULL,
|
||||
[Phone] NVARCHAR(50) NULL,
|
||||
[CreationTime] DATETIME2 NOT NULL CONSTRAINT [DF_Prj_T_Customer_CreationTime] DEFAULT (GETUTCDATE()),
|
||||
[CreatorId] UNIQUEIDENTIFIER NULL,
|
||||
[LastModificationTime] DATETIME2 NULL,
|
||||
[LastModifierId] UNIQUEIDENTIFIER NULL,
|
||||
[IsDeleted] BIT NOT NULL CONSTRAINT [DF_Prj_T_Customer_IsDeleted] DEFAULT (0),
|
||||
[DeletionTime] DATETIME2 NULL,
|
||||
[DeleterId] UNIQUEIDENTIFIER NULL
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Prj_T_Customer_TenantId_Name' AND object_id = OBJECT_ID(N'[dbo].[Prj_T_Customer]'))
|
||||
CREATE INDEX [IX_Prj_T_Customer_TenantId_Name] ON [dbo].[Prj_T_Customer] ([TenantId] ASC, [Name] ASC);
|
||||
GO
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
-- Create Table
|
||||
IF OBJECT_ID(N'[dbo].[Prj_T_Milestone]', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[Prj_T_Milestone] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT [PK_Prj_T_Milestone] PRIMARY KEY,
|
||||
[TenantId] UNIQUEIDENTIFIER NULL,
|
||||
[Name] NVARCHAR(300) NOT NULL,
|
||||
[TargetDate] DATETIME2 NULL,
|
||||
[IsCompleted] BIT NOT NULL CONSTRAINT [DF_Prj_T_Milestone_IsCompleted] DEFAULT (0),
|
||||
[CreationTime] DATETIME2 NOT NULL CONSTRAINT [DF_Prj_T_Milestone_CreationTime] DEFAULT (GETUTCDATE()),
|
||||
[CreatorId] UNIQUEIDENTIFIER NULL,
|
||||
[LastModificationTime] DATETIME2 NULL,
|
||||
[LastModifierId] UNIQUEIDENTIFIER NULL,
|
||||
[IsDeleted] BIT NOT NULL CONSTRAINT [DF_Prj_T_Milestone_IsDeleted] DEFAULT (0),
|
||||
[DeletionTime] DATETIME2 NULL,
|
||||
[DeleterId] UNIQUEIDENTIFIER NULL
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Prj_T_Milestone_TenantId_Name' AND object_id = OBJECT_ID(N'[dbo].[Prj_T_Milestone]'))
|
||||
CREATE INDEX [IX_Prj_T_Milestone_TenantId_Name] ON [dbo].[Prj_T_Milestone] ([TenantId] ASC, [Name] ASC);
|
||||
GO
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
-- Create Table
|
||||
IF OBJECT_ID(N'[dbo].[Prj_T_Tag]', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[Prj_T_Tag] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT [PK_Prj_T_Tag] PRIMARY KEY,
|
||||
[TenantId] UNIQUEIDENTIFIER NULL,
|
||||
[Name] NVARCHAR(200) NOT NULL,
|
||||
[ColorCode] NVARCHAR(20) NULL,
|
||||
[CreationTime] DATETIME2 NOT NULL CONSTRAINT [DF_Prj_T_Tag_CreationTime] DEFAULT (GETUTCDATE()),
|
||||
[CreatorId] UNIQUEIDENTIFIER NULL,
|
||||
[LastModificationTime] DATETIME2 NULL,
|
||||
[LastModifierId] UNIQUEIDENTIFIER NULL,
|
||||
[IsDeleted] BIT NOT NULL CONSTRAINT [DF_Prj_T_Tag_IsDeleted] DEFAULT (0),
|
||||
[DeletionTime] DATETIME2 NULL,
|
||||
[DeleterId] UNIQUEIDENTIFIER NULL
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Prj_T_Tag_TenantId_Name' AND object_id = OBJECT_ID(N'[dbo].[Prj_T_Tag]'))
|
||||
CREATE INDEX [IX_Prj_T_Tag_TenantId_Name] ON [dbo].[Prj_T_Tag] ([TenantId] ASC, [Name] ASC);
|
||||
GO
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
-- Create Table
|
||||
IF OBJECT_ID(N'[dbo].[Prj_T_Task]', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[Prj_T_Task] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT [PK_Prj_T_Task] PRIMARY KEY,
|
||||
[TenantId] UNIQUEIDENTIFIER NULL,
|
||||
[Name] NVARCHAR(300) NOT NULL,
|
||||
[MilestoneId] UNIQUEIDENTIFIER NULL,
|
||||
[AssigneeIds] NVARCHAR(1000) NULL,
|
||||
[TagIds] NVARCHAR(1000) NULL,
|
||||
[CustomerId] UNIQUEIDENTIFIER NULL,
|
||||
[CustomerContact] NVARCHAR(200) NULL,
|
||||
[SalesOrderLine] NVARCHAR(300) NULL,
|
||||
[PlannedHours] DECIMAL(18, 2) NULL,
|
||||
[ProgressPercent] DECIMAL(18, 2) NULL,
|
||||
[Status] NVARCHAR(50) NOT NULL CONSTRAINT [DF_Prj_T_Task_Status] DEFAULT (N'Taslak'),
|
||||
[PlannedStartDate] DATETIME2 NULL,
|
||||
[PlannedEndDate] DATETIME2 NULL,
|
||||
[IsFavorite] BIT NOT NULL CONSTRAINT [DF_Prj_T_Task_IsFavorite] DEFAULT (0),
|
||||
[ParentTaskId] UNIQUEIDENTIFIER NULL,
|
||||
[Description] NVARCHAR(MAX) NULL,
|
||||
[CreationTime] DATETIME2 NOT NULL CONSTRAINT [DF_Prj_T_Task_CreationTime] DEFAULT (GETUTCDATE()),
|
||||
[CreatorId] UNIQUEIDENTIFIER NULL,
|
||||
[LastModificationTime] DATETIME2 NULL,
|
||||
[LastModifierId] UNIQUEIDENTIFIER NULL,
|
||||
[IsDeleted] BIT NOT NULL CONSTRAINT [DF_Prj_T_Task_IsDeleted] DEFAULT (0),
|
||||
[DeletionTime] DATETIME2 NULL,
|
||||
[DeleterId] UNIQUEIDENTIFIER NULL
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_Prj_T_Task_Milestone')
|
||||
ALTER TABLE [dbo].[Prj_T_Task] ADD CONSTRAINT [FK_Prj_T_Task_Milestone]
|
||||
FOREIGN KEY ([MilestoneId]) REFERENCES [dbo].[Prj_T_Milestone] ([Id]);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_Prj_T_Task_Customer')
|
||||
ALTER TABLE [dbo].[Prj_T_Task] ADD CONSTRAINT [FK_Prj_T_Task_Customer]
|
||||
FOREIGN KEY ([CustomerId]) REFERENCES [dbo].[Prj_T_Customer] ([Id]);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_Prj_T_Task_ParentTask')
|
||||
ALTER TABLE [dbo].[Prj_T_Task] ADD CONSTRAINT [FK_Prj_T_Task_ParentTask]
|
||||
FOREIGN KEY ([ParentTaskId]) REFERENCES [dbo].[Prj_T_Task] ([Id]);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Prj_T_Task_TenantId_Status' AND object_id = OBJECT_ID(N'[dbo].[Prj_T_Task]'))
|
||||
CREATE INDEX [IX_Prj_T_Task_TenantId_Status] ON [dbo].[Prj_T_Task] ([TenantId] ASC, [Status] ASC);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Prj_T_Task_ParentTaskId' AND object_id = OBJECT_ID(N'[dbo].[Prj_T_Task]'))
|
||||
CREATE INDEX [IX_Prj_T_Task_ParentTaskId] ON [dbo].[Prj_T_Task] ([ParentTaskId] ASC);
|
||||
GO
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
-- Create Table
|
||||
IF OBJECT_ID(N'[dbo].[Prj_T_TaskDependency]', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[Prj_T_TaskDependency] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT [PK_Prj_T_TaskDependency] PRIMARY KEY,
|
||||
[TenantId] UNIQUEIDENTIFIER NULL,
|
||||
[TaskId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[DependsOnTaskId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[Note] NVARCHAR(300) NULL,
|
||||
[CreationTime] DATETIME2 NOT NULL CONSTRAINT [DF_Prj_T_TaskDependency_CreationTime] DEFAULT (GETUTCDATE()),
|
||||
[CreatorId] UNIQUEIDENTIFIER NULL,
|
||||
[LastModificationTime] DATETIME2 NULL,
|
||||
[LastModifierId] UNIQUEIDENTIFIER NULL,
|
||||
[IsDeleted] BIT NOT NULL CONSTRAINT [DF_Prj_T_TaskDependency_IsDeleted] DEFAULT (0),
|
||||
[DeletionTime] DATETIME2 NULL,
|
||||
[DeleterId] UNIQUEIDENTIFIER NULL
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_Prj_T_TaskDependency_Task')
|
||||
ALTER TABLE [dbo].[Prj_T_TaskDependency] ADD CONSTRAINT [FK_Prj_T_TaskDependency_Task]
|
||||
FOREIGN KEY ([TaskId]) REFERENCES [dbo].[Prj_T_Task] ([Id]);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_Prj_T_TaskDependency_DependsOnTask')
|
||||
ALTER TABLE [dbo].[Prj_T_TaskDependency] ADD CONSTRAINT [FK_Prj_T_TaskDependency_DependsOnTask]
|
||||
FOREIGN KEY ([DependsOnTaskId]) REFERENCES [dbo].[Prj_T_Task] ([Id]);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Prj_T_TaskDependency_TaskId' AND object_id = OBJECT_ID(N'[dbo].[Prj_T_TaskDependency]'))
|
||||
CREATE INDEX [IX_Prj_T_TaskDependency_TaskId] ON [dbo].[Prj_T_TaskDependency] ([TaskId] ASC);
|
||||
GO
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
-- Create Table
|
||||
IF OBJECT_ID(N'[dbo].[Prj_T_Timesheet]', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[Prj_T_Timesheet] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT [PK_Prj_T_Timesheet] PRIMARY KEY,
|
||||
[TenantId] UNIQUEIDENTIFIER NULL,
|
||||
[TaskId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[WorkDate] DATETIME2 NOT NULL,
|
||||
[UserId] UNIQUEIDENTIFIER NULL,
|
||||
[Description] NVARCHAR(500) NULL,
|
||||
[Hours] DECIMAL(18, 2) NOT NULL CONSTRAINT [DF_Prj_T_Timesheet_Hours] DEFAULT (0),
|
||||
[CreationTime] DATETIME2 NOT NULL CONSTRAINT [DF_Prj_T_Timesheet_CreationTime] DEFAULT (GETUTCDATE()),
|
||||
[CreatorId] UNIQUEIDENTIFIER NULL,
|
||||
[LastModificationTime] DATETIME2 NULL,
|
||||
[LastModifierId] UNIQUEIDENTIFIER NULL,
|
||||
[IsDeleted] BIT NOT NULL CONSTRAINT [DF_Prj_T_Timesheet_IsDeleted] DEFAULT (0),
|
||||
[DeletionTime] DATETIME2 NULL,
|
||||
[DeleterId] UNIQUEIDENTIFIER NULL
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_Prj_T_Timesheet_Task')
|
||||
ALTER TABLE [dbo].[Prj_T_Timesheet] ADD CONSTRAINT [FK_Prj_T_Timesheet_Task]
|
||||
FOREIGN KEY ([TaskId]) REFERENCES [dbo].[Prj_T_Task] ([Id]);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Prj_T_Timesheet_TaskId_WorkDate' AND object_id = OBJECT_ID(N'[dbo].[Prj_T_Timesheet]'))
|
||||
CREATE INDEX [IX_Prj_T_Timesheet_TaskId_WorkDate] ON [dbo].[Prj_T_Timesheet] ([TaskId] ASC, [WorkDate] DESC);
|
||||
GO
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
IF OBJECT_ID(N'[dbo].[Sal_D_Appoval]', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[Sal_D_Appoval]
|
||||
(
|
||||
[Id] uniqueidentifier NOT NULL,
|
||||
[UserName] nvarchar(512) NULL,
|
||||
[Status] nvarchar(100) NULL,
|
||||
[Date] datetime NULL,
|
||||
[Description] nvarchar(400) NULL,
|
||||
[Text] nvarchar(100) NULL,
|
||||
CONSTRAINT [PK_Sal_D_Appoval] PRIMARY KEY NONCLUSTERED
|
||||
(
|
||||
[Id] ASC
|
||||
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
|
||||
) ON [PRIMARY]
|
||||
END
|
||||
GO
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,159 +0,0 @@
|
|||
{
|
||||
"Wizard": {
|
||||
"ComponentKind": 1,
|
||||
"WizardName": "RoleList",
|
||||
"WizardCode": "App.Wizard.RoleList",
|
||||
"SeededAt": "2026-09-05T19:53:59.1764998\u002B03:00",
|
||||
"CustomComponentName": "RoleComponent",
|
||||
"PermissionGroupName": "App.Wizard.Sales",
|
||||
"ModuleId": "Sal",
|
||||
"Menu": {
|
||||
"MenuParentCode": "App.Wizard.Sales",
|
||||
"MenuParentIcon": "FcAssistant",
|
||||
"MenuCode": "App.Wizard.RoleList",
|
||||
"MenuOrder": 2,
|
||||
"MenuUrl": "/admin/roles",
|
||||
"MenuIcon": "FcBullish"
|
||||
},
|
||||
"Language": {
|
||||
"MenuParentEn": "Sales",
|
||||
"MenuParentTr": "Sat\u0131\u015F",
|
||||
"PermissionGroupEn": "Sales",
|
||||
"PermissionGroupTr": "Sat\u0131\u015F",
|
||||
"MenuEn": "Role List",
|
||||
"MenuTr": "Role List",
|
||||
"TitleEn": "Role List",
|
||||
"TitleTr": "Role List",
|
||||
"DescEn": "Role List",
|
||||
"DescTr": "Role List"
|
||||
},
|
||||
"Groups": [],
|
||||
"IsTenant": false,
|
||||
"IsBranch": false,
|
||||
"IsOrganizationUnit": false,
|
||||
"AllowAdding": true,
|
||||
"AllowUpdating": true,
|
||||
"AllowDeleting": true,
|
||||
"AllowDetail": false,
|
||||
"ConfirmDelete": true,
|
||||
"DefaultLayout": "grid",
|
||||
"Grid": true,
|
||||
"Card": true,
|
||||
"Pivot": true,
|
||||
"Tree": false,
|
||||
"Chart": true,
|
||||
"Gantt": false,
|
||||
"Scheduler": false,
|
||||
"Todo": false,
|
||||
"DataSourceCode": "Default",
|
||||
"SelectCommandType": 1,
|
||||
"SelectCommand": "",
|
||||
"KeyFieldName": "",
|
||||
"KeyFieldDbSourceType": 11,
|
||||
"TreeOptionDto": {
|
||||
"KeyExpr": "",
|
||||
"ParentIdExpr": "",
|
||||
"HasItemsExpr": "",
|
||||
"RootValue": "",
|
||||
"ExpandedRowKeys": [],
|
||||
"AutoExpandAll": false,
|
||||
"RecursiveSelection": false,
|
||||
"TitleExpr": null,
|
||||
"StartExpr": null,
|
||||
"EndExpr": null,
|
||||
"ProgressExpr": null
|
||||
},
|
||||
"GanttOptionDto": {
|
||||
"KeyExpr": "",
|
||||
"ParentIdExpr": "",
|
||||
"HasItemsExpr": "",
|
||||
"RootValue": "",
|
||||
"ExpandedRowKeys": [],
|
||||
"AutoExpandAll": false,
|
||||
"RecursiveSelection": false,
|
||||
"TitleExpr": "",
|
||||
"ScaleType": "weeks",
|
||||
"StartExpr": "",
|
||||
"EndExpr": "",
|
||||
"ProgressExpr": "",
|
||||
"EditingEnabled": false,
|
||||
"AllowTaskAdding": false,
|
||||
"AllowTaskUpdating": false,
|
||||
"AllowTaskResourceUpdating": false,
|
||||
"AllowTaskDeleting": false,
|
||||
"AllowDependencyAdding": false,
|
||||
"AllowDependencyDeleting": false,
|
||||
"AllowResourceAdding": false,
|
||||
"AllowResourceUpdating": false,
|
||||
"AllowResourceDeleting": false
|
||||
},
|
||||
"SchedulerOptionDto": {
|
||||
"TextExpr": "",
|
||||
"UserNameExpr": null,
|
||||
"DescriptionExpr": null,
|
||||
"StartDateExpr": "",
|
||||
"EndDateExpr": "",
|
||||
"AllDayExpr": "",
|
||||
"RecurrenceRuleExpr": "",
|
||||
"RecurrenceExceptionExpr": "",
|
||||
"StartDayHour": 8,
|
||||
"EndDayHour": 20,
|
||||
"DefaultView": "week",
|
||||
"ShowAllDayPanel": true,
|
||||
"CellDuration": 30,
|
||||
"FirstDayOfWeek": 1,
|
||||
"CrossScrollingEnabled": false,
|
||||
"AllowResizing": false,
|
||||
"AllowDragging": false,
|
||||
"AllowDeleting": false,
|
||||
"AllowEditing": false,
|
||||
"AllowAdding": false
|
||||
},
|
||||
"TodoOptionDto": {
|
||||
"TitleExpr": "",
|
||||
"StatusExpr": "",
|
||||
"DescriptionExpr": "",
|
||||
"DueDateExpr": "",
|
||||
"TagExpr": "",
|
||||
"AssigneeExpr": "",
|
||||
"PriorityExpr": "",
|
||||
"CompletedExpr": "",
|
||||
"OrderExpr": "",
|
||||
"TimesheetExpr": null,
|
||||
"SubTaskExpr": null,
|
||||
"StatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging",
|
||||
"AllowDragging": true
|
||||
},
|
||||
"WorkflowDto": {
|
||||
"ApprovalUserFieldName": "",
|
||||
"ApprovalDateFieldName": "",
|
||||
"ApprovalStatusFieldName": "",
|
||||
"ApprovalDescriptionFieldName": "",
|
||||
"ApprovalIsFilterUserName": false,
|
||||
"ApprovalIsResetWorkflow": false,
|
||||
"Criteria": []
|
||||
},
|
||||
"SubForms": [],
|
||||
"Widgets": []
|
||||
},
|
||||
"ListForm": null,
|
||||
"Fields": [],
|
||||
"IsDeletedField": false,
|
||||
"IsCreatedField": false,
|
||||
"InsertedRecords": {
|
||||
"LanguageKeys": [
|
||||
"App.Wizard.RoleList"
|
||||
],
|
||||
"PermissionGroupNames": [],
|
||||
"PermissionNames": [
|
||||
"App.Wizard.RoleList",
|
||||
"App.Wizard.RoleList.Create",
|
||||
"App.Wizard.RoleList.Update",
|
||||
"App.Wizard.RoleList.Delete"
|
||||
],
|
||||
"MenuCodes": [
|
||||
"App.Wizard.RoleList"
|
||||
],
|
||||
"DataSourceCodes": []
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -141,12 +141,63 @@ export interface WizardFileInfoDto {
|
|||
dependencies: string[]
|
||||
}
|
||||
|
||||
export enum WizardRecordTypeEnum {
|
||||
SeedFile = 0,
|
||||
ListForm = 1,
|
||||
ListFormField = 2,
|
||||
Workflow = 3,
|
||||
PermissionGrant = 4,
|
||||
Permission = 5,
|
||||
PermissionGroup = 6,
|
||||
Menu = 7,
|
||||
Module = 8,
|
||||
DataSource = 9,
|
||||
LanguageKey = 10,
|
||||
CustomEndpoint = 20,
|
||||
BackgroundWorker = 21,
|
||||
IntranetWidget = 22,
|
||||
Tenant = 23,
|
||||
}
|
||||
|
||||
export enum WizardDeleteStatusEnum {
|
||||
Deleted = 0,
|
||||
Kept = 1,
|
||||
NotFound = 2,
|
||||
}
|
||||
|
||||
export enum WizardDeleteReasonEnum {
|
||||
None = 0,
|
||||
InUse = 1,
|
||||
Redeploy = 2,
|
||||
}
|
||||
|
||||
export interface WizardRecordRefDto {
|
||||
recordType: WizardRecordTypeEnum
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface WizardDeleteLogItemDto {
|
||||
recordType: WizardRecordTypeEnum
|
||||
name: string
|
||||
status: WizardDeleteStatusEnum
|
||||
reason: WizardDeleteReasonEnum
|
||||
usedBy: WizardRecordRefDto[]
|
||||
}
|
||||
|
||||
/** Silinen, korunan ve bulunamayan her kayıt tek tek listelenir. */
|
||||
export interface WizardDeleteResultDto {
|
||||
fileName: string
|
||||
wizardName: string
|
||||
items: WizardDeleteLogItemDto[]
|
||||
}
|
||||
|
||||
export interface WizardInsertedRecordsDto {
|
||||
languageKeys: string[]
|
||||
permissionGroupNames: string[]
|
||||
permissionNames: string[]
|
||||
menuCodes: string[]
|
||||
dataSourceCodes: string[]
|
||||
moduleIds: string[]
|
||||
}
|
||||
|
||||
export interface WizardSeedFileItemDto {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
ListFormWizardDto,
|
||||
WizardExportAnalysisDto,
|
||||
WizardDeleteResultDto,
|
||||
WizardFileInfoDto,
|
||||
WizardImportAnalysisDto,
|
||||
WizardImportApplyResultDto,
|
||||
|
|
@ -38,7 +39,7 @@ export const getWizardFile = (fileName: string) =>
|
|||
})
|
||||
|
||||
export const deleteWizardFile = (fileName: string) =>
|
||||
apiService.fetchData({
|
||||
apiService.fetchData<WizardDeleteResultDto>({
|
||||
method: 'DELETE',
|
||||
url: `/api/app/list-form-wizard/file`,
|
||||
params: { fileName },
|
||||
|
|
|
|||
|
|
@ -42,8 +42,12 @@ import type { ListViewLayoutType } from '../edit/types'
|
|||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import {
|
||||
WizardComponentKindEnum,
|
||||
WizardDeleteReasonEnum,
|
||||
WizardDeleteResultDto,
|
||||
WizardDeleteStatusEnum,
|
||||
WizardExportAnalysisDto,
|
||||
WizardFileInfoDto,
|
||||
WizardRecordTypeEnum,
|
||||
} from '@/proxy/admin/wizard/models'
|
||||
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
|
||||
import { usePermission } from '@/utils/hooks/usePermission'
|
||||
|
|
@ -59,6 +63,21 @@ interface ConfirmState {
|
|||
wizardName: string
|
||||
}
|
||||
|
||||
const DELETE_STATUS_ORDER = [
|
||||
WizardDeleteStatusEnum.Deleted,
|
||||
WizardDeleteStatusEnum.Kept,
|
||||
WizardDeleteStatusEnum.NotFound,
|
||||
]
|
||||
|
||||
const DELETE_STATUS_CLASS: Record<WizardDeleteStatusEnum, string> = {
|
||||
[WizardDeleteStatusEnum.Deleted]:
|
||||
'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
||||
[WizardDeleteStatusEnum.Kept]:
|
||||
'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
[WizardDeleteStatusEnum.NotFound]:
|
||||
'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300',
|
||||
}
|
||||
|
||||
interface WizardTreeNode {
|
||||
file: WizardFileInfoDto
|
||||
children: WizardTreeNode[]
|
||||
|
|
@ -147,6 +166,8 @@ const WizardFileManager = () => {
|
|||
analysis: WizardExportAnalysisDto
|
||||
} | null>(null)
|
||||
const [confirm, setConfirm] = useState<ConfirmState | null>(null)
|
||||
// Silme sonrası sunucunun raporu: hangi kayıt silindi, hangisi neden korundu.
|
||||
const [deleteReport, setDeleteReport] = useState<WizardDeleteResultDto | null>(null)
|
||||
const [search, setSearch] = useState('')
|
||||
const [importFile, setImportFile] = useState<File | null>(null)
|
||||
const importInputRef = useRef<HTMLInputElement>(null)
|
||||
|
|
@ -296,7 +317,8 @@ const WizardFileManager = () => {
|
|||
setDeletingFile(confirm.fileName)
|
||||
setConfirm(null)
|
||||
try {
|
||||
await deleteWizardFile(confirm.fileName)
|
||||
const res = await deleteWizardFile(confirm.fileName)
|
||||
setDeleteReport(res.data)
|
||||
toast.push(
|
||||
<Notification type="success" duration={3000}>
|
||||
<strong>{confirm.wizardName}</strong>{' '}
|
||||
|
|
@ -317,6 +339,26 @@ const WizardFileManager = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const recordTypeText = (type: WizardRecordTypeEnum) =>
|
||||
translate(`::App.ListForm.WizardRecordType.${WizardRecordTypeEnum[type]}`)
|
||||
|
||||
const countByStatus = (report: WizardDeleteResultDto, status: WizardDeleteStatusEnum) =>
|
||||
report.items.filter((i) => i.status === status).length
|
||||
|
||||
const sortReportItems = (report: WizardDeleteResultDto) =>
|
||||
[...report.items].sort(
|
||||
(a, b) =>
|
||||
DELETE_STATUS_ORDER.indexOf(a.status) - DELETE_STATUS_ORDER.indexOf(b.status) ||
|
||||
a.recordType - b.recordType,
|
||||
)
|
||||
|
||||
/** Bu wizard'a bağlı, listede var olan wizard'ların adları. */
|
||||
const dependentNames = (f: WizardFileInfoDto) =>
|
||||
(f.dependencies ?? [])
|
||||
.map((d) => files.find((x) => x.fileName.toLowerCase() === d.toLowerCase()))
|
||||
.filter((x): x is WizardFileInfoDto => !!x)
|
||||
.map((x) => x.wizardName || x.fileName)
|
||||
|
||||
const renderActions = (f: WizardFileInfoDto, className: string) => (
|
||||
<div className={className}>
|
||||
{!f.hasInsertedRecords && (
|
||||
|
|
@ -371,7 +413,15 @@ const WizardFileManager = () => {
|
|||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
title={translate('::App.Platform.Delete')}
|
||||
// Bağlı wizard'ı olan silinemez; sunucu da aynı kuralla reddeder.
|
||||
disabled={dependentNames(f).length > 0}
|
||||
title={
|
||||
dependentNames(f).length > 0
|
||||
? translate('::App.ListForm.WizardDeleteBlockedByDependencies', {
|
||||
0: dependentNames(f).join(', '),
|
||||
})
|
||||
: translate('::App.Platform.Delete')
|
||||
}
|
||||
loading={deletingFile === f.fileName}
|
||||
icon={<FaTrash className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
|
||||
|
|
@ -428,6 +478,16 @@ const WizardFileManager = () => {
|
|||
/>
|
||||
)
|
||||
|
||||
// Arama boş dönerse "henüz dosya yok" demek yanıltıcı olur; iki durum ayrı mesaj taşır.
|
||||
const emptyState = tree.length === 0 && !loading && (
|
||||
<div className="text-center py-12">
|
||||
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
||||
<p className="text-sm text-gray-400 text-center py-4">
|
||||
{translate(search.trim() ? '::App.Platform.NoResults' : '::App.ListForm.WizardNoFiles')}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
/** Alt ekranı olan satırın aç/kapa düğmesi; olmayanlarda hizayı koruyan boşluk. */
|
||||
const renderGroupToggle = (row: Pick<WizardTreeRow, 'key' | 'hasChildren'>) => {
|
||||
if (!row.hasChildren) return <span className="w-3.5 shrink-0" />
|
||||
|
|
@ -547,14 +607,7 @@ const WizardFileManager = () => {
|
|||
loading={loading}
|
||||
className={classNames('mt-4', loading && 'min-h-[240px]')}
|
||||
>
|
||||
{tree.length === 0 && !loading && (
|
||||
<div className="text-center py-12">
|
||||
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
||||
<p className="text-sm text-gray-400 text-center py-4">
|
||||
{translate('::App.ListForm.WizardNoFiles')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{viewMode === 'card' && emptyState}
|
||||
|
||||
{viewMode === 'card' ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
|
|
@ -744,6 +797,11 @@ const WizardFileManager = () => {
|
|||
</tr>
|
||||
)
|
||||
})}
|
||||
{emptyState && (
|
||||
<tr className="border-t border-gray-200 dark:border-gray-700">
|
||||
<td colSpan={7}>{emptyState}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -788,6 +846,98 @@ const WizardFileManager = () => {
|
|||
</Dialog.Footer>
|
||||
</Dialog>
|
||||
|
||||
{/* Silme raporu: kayıtlar önce duruma, sonra türe göre sıralanır. */}
|
||||
<Dialog
|
||||
isOpen={deleteReport !== null}
|
||||
width={860}
|
||||
onClose={() => setDeleteReport(null)}
|
||||
onRequestClose={() => setDeleteReport(null)}
|
||||
>
|
||||
{deleteReport && (
|
||||
<>
|
||||
<Dialog.Body>
|
||||
<p className="font-semibold text-gray-800 dark:text-gray-200">
|
||||
{translate('::App.ListForm.WizardDeleteReport')} — {deleteReport.wizardName}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
{translate('::App.ListForm.WizardDeleteSummary', {
|
||||
0: countByStatus(deleteReport, WizardDeleteStatusEnum.Deleted),
|
||||
1: countByStatus(deleteReport, WizardDeleteStatusEnum.Kept),
|
||||
2: countByStatus(deleteReport, WizardDeleteStatusEnum.NotFound),
|
||||
})}
|
||||
</p>
|
||||
<div className="mt-3 max-h-[60vh] overflow-auto rounded border border-gray-200 dark:border-gray-700">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="sticky top-0 bg-gray-50 text-gray-600 dark:bg-gray-800 dark:text-gray-300">
|
||||
<tr>
|
||||
<th className="px-3 py-1.5 text-left font-medium">
|
||||
{translate('::App.Platform.Status')}
|
||||
</th>
|
||||
<th className="px-3 py-1.5 text-left font-medium">
|
||||
{translate('::App.ListForm.WizardDeleteRecordType')}
|
||||
</th>
|
||||
<th className="px-3 py-1.5 text-left font-medium">
|
||||
{translate('::App.Platform.Name')}
|
||||
</th>
|
||||
<th className="px-3 py-1.5 text-left font-medium">
|
||||
{translate('::App.ListForm.WizardDeleteUsedBy')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortReportItems(deleteReport).map((item, index) => (
|
||||
<tr
|
||||
key={`${item.recordType}:${item.name}:${index}`}
|
||||
className="border-t border-gray-100 align-top dark:border-gray-800"
|
||||
>
|
||||
<td className="whitespace-nowrap px-3 py-1.5">
|
||||
<span
|
||||
className={classNames(
|
||||
'inline-flex rounded-full px-2 py-0.5 font-medium',
|
||||
DELETE_STATUS_CLASS[item.status],
|
||||
)}
|
||||
>
|
||||
{translate(
|
||||
`::App.ListForm.WizardDeleteStatus.${WizardDeleteStatusEnum[item.status]}`,
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-1.5 text-gray-600 dark:text-gray-300">
|
||||
{recordTypeText(item.recordType)}
|
||||
</td>
|
||||
<td className="break-all px-3 py-1.5 font-mono text-gray-800 dark:text-gray-200">
|
||||
{item.name}
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-gray-600 dark:text-gray-300">
|
||||
{item.reason !== WizardDeleteReasonEnum.None && (
|
||||
<div className="text-amber-700 dark:text-amber-300">
|
||||
{translate(
|
||||
`::App.ListForm.WizardDeleteReason.${WizardDeleteReasonEnum[item.reason]}`,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{item.usedBy.map((ref) => (
|
||||
<div key={`${ref.recordType}:${ref.name}`} className="break-all">
|
||||
{recordTypeText(ref.recordType)}:{' '}
|
||||
<span className="font-mono">{ref.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer className="flex justify-end">
|
||||
<Button size="sm" variant="solid" type="button" onClick={() => setDeleteReport(null)}>
|
||||
{translate('::App.Platform.Close')}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
|
||||
{/* Eksik bagimlilikli export onizlemesi */}
|
||||
<Dialog
|
||||
isOpen={exportPreview !== null}
|
||||
|
|
|
|||
Loading…
Reference in a new issue