diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs index bbab7e2..5f682f0 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs @@ -1,9 +1,12 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; namespace Sozsoft.Platform.ListForms; public interface IListFormWizardAppService { Task Create(ListFormWizardDto input); + Task> GetFiles(); + Task DeleteFile(string fileName); } diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs new file mode 100644 index 0000000..abb2ba7 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; + +namespace Sozsoft.Platform.ListForms; + +/// +/// Wizard seed dosyası formatı. +/// WizardAppService tarafından Seeds/WizardData/{wizardName}.json olarak kaydedilir. +/// WizardDataSeeder (DbMigrator) bu dosyaları okuyarak veritabanını yeniden oluşturur. +/// +public class WizardSeedFileDto +{ + public ListFormWizardDto Wizard { get; set; } + + /// Tabloda IsDeleted alanı olup olmadığını belirtir (soft delete desteği). + public bool IsDeletedField { get; set; } + + /// Tabloda CreatorId alanı olup olmadığını belirtir (audit alanı). + public bool IsCreatedField { get; set; } + + /// + /// Bu Wizard çalışırken gerçekten YENİ oluşturulan kayıtlar. + /// Daha önce var olan kayıtlar buraya eklenmez, silme işleminde sadece bu liste kullanılır. + /// + public WizardInsertedRecordsDto InsertedRecords { get; set; } = new(); +} + +/// +/// Wizard Create sırasında gerçekten veritabanına eklenen kayıtların izleme listesi. +/// Silme işleminde sadece bu listeler kullanılır; paylaşılan kayıtlara dokunulmaz. +/// +public class WizardInsertedRecordsDto +{ + /// Bu Wizard tarafından oluşturulan yeni LanguageKey değerleri (Key alanı). + public List LanguageKeys { get; set; } = []; + + /// Bu Wizard tarafından oluşturulan yeni PermissionGroup isimleri. + public List PermissionGroupNames { get; set; } = []; + + /// Bu Wizard tarafından oluşturulan yeni Permission isimleri. + public List PermissionNames { get; set; } = []; + + /// Bu Wizard tarafından oluşturulan yeni Menu kodları. + public List MenuCodes { get; set; } = []; + + /// Bu Wizard tarafından oluşturulan yeni DataSource kodları. + public List DataSourceCodes { get; set; } = []; +} + +/// Wizard seed dosyası özet bilgisi (yönetim listesi için). +public class WizardFileInfoDto +{ + public string FileName { get; set; } + public string WizardName { get; set; } + public string ListFormCode { get; set; } + public string CreatedAt { get; set; } + public bool HasInsertedRecords { get; set; } +} diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormImportAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormImportAppService.cs index bc1335c..5791784 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormImportAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormImportAppService.cs @@ -20,7 +20,7 @@ using static Sozsoft.Platform.PlatformConsts; namespace Sozsoft.Platform.ListForms.ImportManager; -[Authorize()] +[Authorize] public class ListFormImportAppService : PlatformAppService, IImportAppService { private readonly IRepository _importSessionRepository; diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs index 7cab680..5823cd8 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -7,20 +8,20 @@ using Sozsoft.Languages.Entities; using Sozsoft.Languages.Languages; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Enums; -using Microsoft.AspNetCore.Identity; using Volo.Abp.Domain.Repositories; -using Volo.Abp.Identity; using Volo.Abp.MultiTenancy; using Volo.Abp.PermissionManagement; using Volo.Abp.Uow; using static Sozsoft.Platform.PlatformConsts; using System.Data; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Sozsoft.Languages; using Sozsoft.Platform.DynamicData; +using Microsoft.AspNetCore.Authorization; namespace Sozsoft.Platform.ListForms; +[Authorize] public class ListFormWizardAppService( IRepository repoListForm, IRepository repoListFormField, @@ -31,7 +32,7 @@ public class ListFormWizardAppService( IRepository repoPermGroup, IRepository repoMenu, IPermissionGrantRepository permissionGrantRepository, - IConfiguration configuration, + IHostEnvironment hostEnvironment, LanguageTextAppService languageTextAppService, IDynamicDataManager dynamicDataManager ) : PlatformAppService(), IListFormWizardAppService @@ -45,7 +46,7 @@ public class ListFormWizardAppService( private readonly IRepository repoPermGroup = repoPermGroup; private readonly IRepository repoMenu = repoMenu; private readonly IPermissionGrantRepository permissionGrantRepository = permissionGrantRepository; - private readonly IConfiguration _configuration = configuration; + private readonly IHostEnvironment _hostEnvironment = hostEnvironment; private readonly LanguageTextAppService _languageTextAppService = languageTextAppService; private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager; private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage; @@ -59,17 +60,21 @@ public class ListFormWizardAppService( var descLangKey = WizardConsts.WizardKeyDesc(wizardName); var code = WizardConsts.WizardKey(wizardName); + // Eklenen kayıtları takip et (silme işleminde kullanılır) + var inserted = new WizardInsertedRecordsDto(); + //Dil - Language Keys - await CreateLangKey(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr); - await CreateLangKey(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr); - await CreateLangKey(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr); + await CreateLangKey(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr, inserted); + await CreateLangKey(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr, inserted); + await CreateLangKey(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr, inserted); //Permission Group var groupName = input.PermissionGroupName ?? PlatformConsts.AppName; if (!await repoPermGroup.AnyAsync(a => a.Name == groupName)) { await repoPermGroup.InsertAsync(new PermissionGroupDefinitionRecord(GuidGenerator.Create(), groupName, groupName), autoSave: false); - await CreateLangKey(groupName, groupName, groupName); + await CreateLangKey(groupName, groupName, groupName, inserted); + inserted.PermissionGroupNames.Add(groupName); } // Permission'ları tek seferde kontrol et ve oluştur @@ -78,26 +83,54 @@ public class ListFormWizardAppService( queryable.Where(a => a.GroupName == groupName) ); - var permRead = existingPerms.FirstOrDefault(a => a.Name == code) ?? - await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both), autoSave: false); + var permRead = existingPerms.FirstOrDefault(a => a.Name == code); + if (permRead == null) + { + permRead = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both), autoSave: false); + inserted.PermissionNames.Add(permRead.Name); + } - var permCreate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermCreate(wizardName)) ?? - await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermCreate(wizardName), permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: false); + var permCreate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermCreate(wizardName)); + if (permCreate == null) + { + permCreate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermCreate(wizardName), permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: false); + inserted.PermissionNames.Add(permCreate.Name); + } - var permUpdate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermUpdate(wizardName)) ?? - await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermUpdate(wizardName), permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: false); + var permUpdate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermUpdate(wizardName)); + if (permUpdate == null) + { + permUpdate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermUpdate(wizardName), permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: false); + inserted.PermissionNames.Add(permUpdate.Name); + } - var permDelete = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermDelete(wizardName)) ?? - await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermDelete(wizardName), permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: false); + var permDelete = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermDelete(wizardName)); + if (permDelete == null) + { + permDelete = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermDelete(wizardName), permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: false); + inserted.PermissionNames.Add(permDelete.Name); + } - var permExport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermExport(wizardName)) ?? - await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermExport(wizardName), permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: false); + var permExport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermExport(wizardName)); + if (permExport == null) + { + permExport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermExport(wizardName), permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: false); + inserted.PermissionNames.Add(permExport.Name); + } - var permImport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermImport(wizardName)) ?? - await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermImport(wizardName), permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: false); + var permImport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermImport(wizardName)); + if (permImport == null) + { + permImport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermImport(wizardName), permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: false); + inserted.PermissionNames.Add(permImport.Name); + } - var permNote = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermNote(wizardName)) ?? - await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermNote(wizardName), permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: false); + var permNote = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermNote(wizardName)); + if (permNote == null) + { + permNote = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermNote(wizardName), permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: false); + inserted.PermissionNames.Add(permNote.Name); + } // Permission Grants - Bulk Insert (only missing ones) var existingGrants = await permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName); @@ -122,17 +155,23 @@ public class ListFormWizardAppService( var menuParent = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == input.MenuParentCode)); if (menuParent == null) { - await CreateLangKey(WizardConsts.WizardKeyParent(wizardName), input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr); + var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0; + await CreateLangKey(WizardConsts.WizardKeyParent(wizardName), input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr, inserted); menuParent = await repoMenu.InsertAsync(new Menu { Code = input.MenuParentCode, DisplayName = WizardConsts.WizardKeyParent(wizardName), IsDisabled = false, + Order = maxRootOrder + 1, }, autoSave: false); + inserted.MenuCodes.Add(input.MenuParentCode); } //Menu - var menu = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == code)) ?? + var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0; + var existingMenu = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == code)); + if (existingMenu == null) + { await repoMenu.InsertAsync(new Menu { Code = code, @@ -144,8 +183,11 @@ public class ListFormWizardAppService( ElementId = null, CssClass = null, Url = WizardConsts.MenuUrl(code), - RequiredPermissionName = permRead.Name + RequiredPermissionName = permRead.Name, + Order = maxChildOrder + 1, }, autoSave: false); + inserted.MenuCodes.Add(code); + } //DataSource kodu ile iligli kod blogu var dataSourceQueryable = await repoDataSource.GetQueryableAsync(); @@ -158,6 +200,7 @@ public class ListFormWizardAppService( DataSourceType = input.DataSourceConnectionString.IndexOf("Server") >= 0 ? DataSourceTypeEnum.Mssql : DataSourceTypeEnum.Postgresql, ConnectionString = input.DataSourceConnectionString }, autoSave: false); + inserted.DataSourceCodes.Add(input.DataSourceCode); } // Build EditingFormJson from wizard groups @@ -265,13 +308,209 @@ public class ListFormWizardAppService( LookupJson = item.LookupQuery.Length > 0 ? WizardConsts.DefaultLookupJson(item.LookupDataSourceType, item.DisplayExpr, item.ValueExpr, item.LookupQuery) : null, }, autoSave: true); - await CreateLangKey(item.CaptionName, item.EnglishCaption, item.TurkishCaption); + await CreateLangKey(item.CaptionName, item.EnglishCaption, item.TurkishCaption, inserted); } } // Clear Redis Cache await _languageTextAppService.ClearRedisCacheAsync(); + // Wizard konfigürasyonunu seed dosyasına kaydet + await SaveWizardSeedFileAsync(input, isDeleted, isCreated, inserted); + } + + /// + /// Wizard konfigürasyonunu JSON dosyası olarak kaydeder. + /// Önce ContentRootPath'ten yukarı çıkarak Sozsoft.Platform.DbMigrator/Seeds/WizardData dizinini arar. + /// Bulamazsa ContentRootPath/Seeds/WizardData altına yazar. + /// Veritabanı silinip yeniden oluşturulduğunda WizardDataSeeder bu dosyaları okuyarak konfigürasyonu geri yükler. + /// + private async Task SaveWizardSeedFileAsync(ListFormWizardDto input, bool isDeletedField, bool isCreatedField, WizardInsertedRecordsDto inserted) + { + try + { + var outputPath = ResolveWizardSeedOutputPath(); + Directory.CreateDirectory(outputPath); + + var seedData = new WizardSeedFileDto + { + Wizard = input, + IsDeletedField = isDeletedField, + IsCreatedField = isCreatedField, + InsertedRecords = inserted + }; + + var json = JsonSerializer.Serialize(seedData, new JsonSerializerOptions { WriteIndented = true }); + var safeWizardName = string.Concat(input.WizardName.Trim().Split(Path.GetInvalidFileNameChars())); + var timestamp = DateTime.Now.ToString("yyyyMMddHHmm"); + var filePath = Path.Combine(outputPath, $"{timestamp}_{safeWizardName}.json"); + await File.WriteAllTextAsync(filePath, json); + + Console.WriteLine($"[WizardSeed] Seed dosyası kaydedildi: {filePath}"); + } + catch (Exception ex) + { + // Dosya kaydetme hatası wizard işlemini engellemez + Console.WriteLine($"[WizardSeed] Seed dosyası kaydedilemedi: {ex.Message}"); + } + } + + public Task> GetFiles() + { + var outputPath = ResolveWizardSeedOutputPath(); + var result = new List(); + + if (!Directory.Exists(outputPath)) + return Task.FromResult(result); + + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + + foreach (var file in Directory.GetFiles(outputPath, "*.json").OrderBy(f => Path.GetFileName(f))) + { + try + { + var json = File.ReadAllText(file); + var seed = JsonSerializer.Deserialize(json, options); + var fileName = Path.GetFileName(file); + result.Add(new WizardFileInfoDto + { + FileName = fileName, + WizardName = seed?.Wizard?.WizardName ?? fileName, + ListFormCode = seed?.Wizard?.ListFormCode ?? string.Empty, + CreatedAt = fileName.Length >= 12 ? fileName[..12] : fileName, + 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) + }); + } + catch + { + result.Add(new WizardFileInfoDto { FileName = Path.GetFileName(file) }); + } + } + + return Task.FromResult(result); + } + + public async Task DeleteFile(string fileName) + { + // Güvenlik: sadece dosya adı, path traversal yasak + if (fileName.Contains('/') || fileName.Contains('\\') || fileName.Contains("..")) + throw new Volo.Abp.AbpException("Geçersiz dosya adı."); + + var outputPath = ResolveWizardSeedOutputPath(); + var filePath = Path.Combine(outputPath, fileName); + + if (!File.Exists(filePath)) + throw new Volo.Abp.AbpException($"Dosya bulunamadı: {fileName}"); + + var json = await File.ReadAllTextAsync(filePath); + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var seed = JsonSerializer.Deserialize(json, options); + + if (seed != null) + await DeleteWizardDataAsync(seed); + + File.Delete(filePath); + + await _languageTextAppService.ClearRedisCacheAsync(); + } + + private async Task DeleteWizardDataAsync(WizardSeedFileDto seed) + { + var ins = seed.InsertedRecords; + var listFormCode = seed.Wizard?.ListFormCode; + + // ListForm ve alanları her zaman sil (wizard bunları her zaman oluşturur) + if (!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) + { + 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) + { + 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) + { + 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); + } + } + + /// + /// DbMigrator projesinin Seeds/WizardData dizinini ContentRootPath'ten yukarı traversal ile bulur. + /// Tüm işletim sistemlerinde Path.Combine kullanır, separator karakteri içermez. + /// + private string ResolveWizardSeedOutputPath() + { + const string dbMigratorName = "Sozsoft.Platform.DbMigrator"; + var dir = new DirectoryInfo(_hostEnvironment.ContentRootPath); + + while (dir != null) + { + // src/Sozsoft.Platform.DbMigrator/Seeds altında ara + var candidate = Path.Combine(dir.FullName, "src", dbMigratorName, "Seeds"); + if (Directory.Exists(candidate)) + return Path.Combine(candidate, "WizardData"); + + // Sozsoft.Platform.DbMigrator/Seeds doğrudan altında ara + candidate = Path.Combine(dir.FullName, dbMigratorName, "Seeds"); + if (Directory.Exists(candidate)) + return Path.Combine(candidate, "WizardData"); + + dir = dir.Parent; + } + + // Fallback: çalışan API'nin yanında Seeds/WizardData + return Path.Combine(_hostEnvironment.ContentRootPath, "Seeds", "WizardData"); } private async Task> GetTableColumnNamesAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand) @@ -312,23 +551,27 @@ public class ListFormWizardAppService( } } - private async Task CreateLangKey(string key, string textEn, string textTr) + private async Task CreateLangKey(string key, string textEn, string textTr, WizardInsertedRecordsDto inserted = null) { var res = PlatformConsts.AppName; - var langKey = await repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == res && a.Key == key) - ?? await repoLangKey.InsertAsync(new LanguageKey { ResourceName = res, Key = key }, autoSave: true); + var existing = await repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == res && a.Key == key); + if (existing == null) + { + existing = await repoLangKey.InsertAsync(new LanguageKey { ResourceName = res, Key = key }, autoSave: true); + inserted?.LanguageKeys.Add(key); + } - var existingTexts = await repoLangText.GetListAsync(a => a.ResourceName == res && a.Key == langKey.Key); + var existingTexts = await repoLangText.GetListAsync(a => a.ResourceName == res && a.Key == existing.Key); var existingEn = existingTexts.FirstOrDefault(a => a.CultureName == cultureNameDefault); if (existingEn != null) await repoLangText.DeleteAsync(existingEn, autoSave: true); - await repoLangText.InsertAsync(new LanguageText { ResourceName = res, Key = langKey.Key, CultureName = cultureNameDefault, Value = textEn }, autoSave: true); + await repoLangText.InsertAsync(new LanguageText { ResourceName = res, Key = existing.Key, CultureName = cultureNameDefault, Value = textEn }, autoSave: true); var existingTr = existingTexts.FirstOrDefault(a => a.CultureName == LanguageCodes.Tr); if (existingTr != null) await repoLangText.DeleteAsync(existingTr, autoSave: true); - await repoLangText.InsertAsync(new LanguageText { ResourceName = res, Key = langKey.Key, CultureName = LanguageCodes.Tr, Value = textTr }, autoSave: true); + await repoLangText.InsertAsync(new LanguageText { ResourceName = res, Key = existing.Key, CultureName = LanguageCodes.Tr, Value = textTr }, autoSave: true); - return langKey; + return existing; } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index cc759e4..fdf63df 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -15998,6 +15998,12 @@ "en": "Listform Wizard", "tr": "Listform Sihirbazı" }, + { + "resourceName": "Platform", + "key": "App.Listforms.WizardManager", + "en": "Listform Wizard Manager", + "tr": "Listform Sihirbazı Yöneticisi" + }, { "resourceName": "Platform", "key": "ListForms.Wizard.MenuInfo", @@ -16400,6 +16406,12 @@ "en": "Menu Information", "tr": "Menü Bilgileri" }, + { + "resourceName": "Platform", + "key": "ListForms.Wizard.Step4.MenuIcon", + "en": "Menu Icon", + "tr": "Menü Ikonu" + }, { "resourceName": "Platform", "key": "ListForms.Wizard.Step4.ListFormSettings", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json index 1ddedc3..ef94030 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json @@ -157,7 +157,14 @@ { "key": "admin.listFormManagement.wizard", "path": "/admin/listform/wizard", - "componentPath": "@/views/admin/listForm/Wizard", + "componentPath": "@/views/admin/listForm/wizard/Wizard", + "routeType": "protected", + "authority": ["App.Listforms.Wizard"] + }, + { + "key": "admin.listFormManagement.wizardManager", + "path": "/admin/listform/wizardManager", + "componentPath": "@/views/admin/listForm/wizard/WizardFileManager", "routeType": "protected", "authority": ["App.Listforms.Wizard"] }, @@ -877,10 +884,10 @@ }, { "ParentCode": "App.DeveloperKit", - "Code": "App.Listforms.Wizard", - "DisplayName": "App.Listforms.Wizard", + "Code": "App.Listforms.WizardManager", + "DisplayName": "App.Listforms.WizardManager", "Order": 6, - "Url": "/admin/listform/wizard", + "Url": "/admin/listform/wizardManager", "Icon": "FcFlashAuto", "RequiredPermissionName": "App.Listforms.Wizard", "IsDisabled": false diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/WizardDataSeeder.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/WizardDataSeeder.cs new file mode 100644 index 0000000..2458ea2 --- /dev/null +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/WizardDataSeeder.cs @@ -0,0 +1,379 @@ +using System; +using System.Data; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Sozsoft.Languages.Entities; +using Sozsoft.Languages.Languages; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Enums; +using Sozsoft.Platform.ListForms; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.PermissionManagement; +using static Sozsoft.Platform.PlatformConsts; + +namespace Sozsoft.Platform.Data.Seeds; + +/// +/// Wizard ile oluşturulan konfigürasyonları Seeds/WizardData/*.json dosyalarından okuyarak veritabanına aktarır. +/// Wizard çalıştıktan sonra oluşturulan JSON dosyaları projeye commit edildikten sonra, +/// veritabanı silinip yeniden oluşturulduğunda bu seeder tüm wizard konfigürasyonlarını geri yükler. +/// +public class WizardDataSeeder : IDataSeedContributor, ITransientDependency +{ + private readonly IRepository _repoLangKey; + private readonly IRepository _repoLangText; + private readonly IRepository _repoPermGroup; + private readonly IRepository _repoPerm; + private readonly IPermissionGrantRepository _permissionGrantRepository; + private readonly IRepository _repoMenu; + private readonly IRepository _repoDataSource; + private readonly IRepository _repoListForm; + private readonly IRepository _repoListFormField; + private readonly ILogger _logger; + + private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage; + private readonly string _appName = PlatformConsts.AppName; + + public WizardDataSeeder( + IRepository repoLangKey, + IRepository repoLangText, + IRepository repoPermGroup, + IRepository repoPerm, + IPermissionGrantRepository permissionGrantRepository, + IRepository repoMenu, + IRepository repoDataSource, + IRepository repoListForm, + IRepository repoListFormField, + ILogger logger) + { + _repoLangKey = repoLangKey; + _repoLangText = repoLangText; + _repoPermGroup = repoPermGroup; + _repoPerm = repoPerm; + _permissionGrantRepository = permissionGrantRepository; + _repoMenu = repoMenu; + _repoDataSource = repoDataSource; + _repoListForm = repoListForm; + _repoListFormField = repoListFormField; + _logger = logger; + } + + public async Task SeedAsync(DataSeedContext context) + { + var wizardDataPath = Path.Combine(Directory.GetCurrentDirectory(), "Seeds", "WizardData"); + if (!Directory.Exists(wizardDataPath)) + { + _logger.LogInformation("Seeds/WizardData dizini bulunamadı, atlanıyor."); + return; + } + + var jsonFiles = Directory.GetFiles(wizardDataPath, "*.json").OrderBy(f => Path.GetFileName(f)).ToArray(); + if (jsonFiles.Length == 0) + { + _logger.LogInformation("Seeds/WizardData dizininde JSON dosyası bulunamadı, atlanıyor."); + return; + } + + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + + foreach (var filePath in jsonFiles) + { + try + { + var json = await File.ReadAllTextAsync(filePath); + var seedFile = JsonSerializer.Deserialize(json, options); + + if (seedFile?.Wizard == null) + { + _logger.LogWarning($"Geçersiz dosya atlandı: {filePath}"); + continue; + } + + var wizardName = seedFile.Wizard.WizardName?.Trim(); + if (string.IsNullOrWhiteSpace(wizardName)) + { + _logger.LogWarning($"WizardName boş olduğu için atlandı: {filePath}"); + continue; + } + + // Zaten seeded mi kontrol et (ListForm var mı?) + if (await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode)) + { + _logger.LogInformation($"'{wizardName}' zaten mevcut, atlandı."); + continue; + } + + _logger.LogInformation($"'{wizardName}' uygulanıyor..."); + await ApplyWizardSeedAsync(seedFile); + _logger.LogInformation($"'{wizardName}' başarıyla uygulandı."); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Hata - {filePath}: {ex.Message}"); + } + } + } + + private async Task ApplyWizardSeedAsync(WizardSeedFileDto seedFile) + { + var input = seedFile.Wizard; + var isDeleted = seedFile.IsDeletedField; + var isCreated = seedFile.IsCreatedField; + + var wizardName = input.WizardName.Trim(); + var titleLangKey = WizardConsts.WizardKeyTitle(wizardName); + var nameLangKey = WizardConsts.WizardKey(wizardName); + var descLangKey = WizardConsts.WizardKeyDesc(wizardName); + var code = WizardConsts.WizardKey(wizardName); + + // Dil - Language Keys + await CreateLangKeyAsync(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr); + await CreateLangKeyAsync(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr); + await CreateLangKeyAsync(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr); + + // Permission Group + var groupName = input.PermissionGroupName ?? AppName; + if (!await _repoPermGroup.AnyAsync(a => a.Name == groupName)) + { + await _repoPermGroup.InsertAsync( + new PermissionGroupDefinitionRecord(Guid.NewGuid(), groupName, groupName), autoSave: true); + await CreateLangKeyAsync(groupName, groupName, groupName); + } + + // Permissions + var permQueryable = await _repoPerm.GetQueryableAsync(); + var existingPerms = permQueryable.Where(a => a.GroupName == groupName).ToList(); + + var permRead = existingPerms.FirstOrDefault(a => a.Name == code) ?? + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both), autoSave: true); + + var permCreate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermCreate(wizardName)) ?? + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, WizardConsts.PermCreate(wizardName), permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true); + + var permUpdate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermUpdate(wizardName)) ?? + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, WizardConsts.PermUpdate(wizardName), permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true); + + var permDelete = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermDelete(wizardName)) ?? + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, WizardConsts.PermDelete(wizardName), permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true); + + var permExport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermExport(wizardName)) ?? + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, WizardConsts.PermExport(wizardName), permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true); + + var permImport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermImport(wizardName)) ?? + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, WizardConsts.PermImport(wizardName), permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true); + + var permNote = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermNote(wizardName)) ?? + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, WizardConsts.PermNote(wizardName), permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true); + + // Permission Grants - Admin role için + var existingGrants = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName); + var existingGrantNames = existingGrants.Select(g => g.Name).ToHashSet(); + + var grantsToInsert = new[] + { + permRead.Name, permCreate.Name, permUpdate.Name, + permDelete.Name, permExport.Name, permImport.Name, permNote.Name + } + .Where(name => !existingGrantNames.Contains(name)) + .Select(name => new PermissionGrant(Guid.NewGuid(), name, "R", PlatformConsts.AbpIdentity.User.AdminRoleName)) + .ToList(); + + if (grantsToInsert.Count > 0) + { + await _permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: true); + } + + // Menu Parent + var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode); + if (menuParent == null) + { + await CreateLangKeyAsync(WizardConsts.WizardKeyParent(wizardName), input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr); + menuParent = await _repoMenu.InsertAsync(new Menu + { + Code = input.MenuParentCode, + DisplayName = WizardConsts.WizardKeyParent(wizardName), + IsDisabled = false, + }, autoSave: true); + } + + // Menu + if (!await _repoMenu.AnyAsync(a => a.Code == code)) + { + await _repoMenu.InsertAsync(new Menu + { + Code = code, + DisplayName = nameLangKey, + IsDisabled = false, + ParentCode = menuParent.Code, + Icon = input.MenuIcon ?? WizardConsts.MenuIcon, + Target = null, + ElementId = null, + CssClass = null, + Url = WizardConsts.MenuUrl(code), + RequiredPermissionName = permRead.Name + }, autoSave: true); + } + + // DataSource + if (!await _repoDataSource.AnyAsync(a => a.Code == input.DataSourceCode)) + { + await _repoDataSource.InsertAsync(new DataSource + { + Code = input.DataSourceCode, + DataSourceType = input.DataSourceConnectionString != null && + input.DataSourceConnectionString.IndexOf("Server", StringComparison.OrdinalIgnoreCase) >= 0 + ? DataSourceTypeEnum.Mssql + : DataSourceTypeEnum.Postgresql, + ConnectionString = input.DataSourceConnectionString + }, autoSave: true); + } + + // EditingFormJson + var editingFormDtos = input.Groups + .Select((g, gi) => new EditingFormDto + { + Order = gi + 1, + Caption = g.Caption, + ColCount = g.ColCount, + ColSpan = g.ColCount, + ItemType = "group", + Items = g.Items + .Where(i => i.DataField != input.KeyFieldName) + .Select((it, ii) => new EditingFormItemDto + { + Order = ii + 1, + DataField = it.DataField, + EditorType2 = it.EditorType, + ColSpan = it.ColSpan, + EditorOptions = string.IsNullOrWhiteSpace(it.EditorOptions) ? null : it.EditorOptions, + EditorScript = string.IsNullOrWhiteSpace(it.EditorScript) ? null : it.EditorScript, + IsRequired = it.IsRequired, + }) + .ToArray() + }) + .ToList(); + + // ListForm + await _repoListForm.InsertAsync(new ListForm + { + ListFormType = ListFormTypeEnum.List, + PageSize = 10, + ExportJson = WizardConsts.DefaultExportJson, + IsSubForm = false, + ShowNote = true, + LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler), + CultureName = LanguageCodes.En, + ListFormCode = input.ListFormCode, + Name = nameLangKey, + Title = titleLangKey, + Description = descLangKey, + DataSourceCode = input.DataSourceCode, + IsTenant = input.IsTenant, + IsBranch = input.IsBranch, + IsOrganizationUnit = input.IsOrganizationUnit, + SelectCommandType = input.SelectCommandType, + SelectCommand = input.SelectCommand, + KeyFieldName = input.KeyFieldName, + KeyFieldDbSourceType = input.KeyFieldDbSourceType, + DefaultFilter = isDeleted ? WizardConsts.DefaultFilterJson : null, + SortMode = GridOptions.SortModeSingle, + FilterRowJson = WizardConsts.DefaultFilterRowJson, + HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson, + SearchPanelJson = WizardConsts.DefaultSearchPanelJson, + GroupPanelJson = JsonSerializer.Serialize(new { Visible = false }), + SelectionJson = WizardConsts.DefaultSelectionSingleJson, + ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(), + PermissionJson = WizardConsts.DefaultPermissionJson(code), + DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null, + DeleteFieldsDefaultValueJson = isDeleted + ? WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType) + : WizardConsts.DefaultFieldsJsonOnlyId(input.KeyFieldDbSourceType), + InsertFieldsDefaultValueJson = isCreated + ? WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType) + : WizardConsts.DefaultFieldsJsonOnlyId(input.KeyFieldDbSourceType), + PagerOptionJson = WizardConsts.DefaultPagerOptionJson, + EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, 600, 500, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail), + EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null, + }, autoSave: true); + + // ListFormFields + var fieldOrder = 0; + foreach (var group in input.Groups) + { + foreach (var item in group.Items) + { + fieldOrder++; + await _repoListFormField.InsertAsync(new ListFormField + { + ListFormCode = input.ListFormCode, + FieldName = item.DataField, + CaptionName = item.CaptionName, + Visible = item.DataField != input.KeyFieldName, + IsActive = true, + AllowSearch = true, + ListOrderNo = fieldOrder, + SourceDbType = item.DbSourceType, + CultureName = PlatformConsts.DefaultLanguage, + PermissionJson = WizardConsts.DefaultFieldPermissionJson(code), + ColumnCustomizationJson = WizardConsts.DefaultColumnCustomizationJson, + ColumnFilterJson = WizardConsts.DefaultColumnFilteringJson, + PivotSettingsJson = WizardConsts.DefaultPivotSettingsJson, + LookupJson = !string.IsNullOrWhiteSpace(item.LookupQuery) + ? WizardConsts.DefaultLookupJson(item.LookupDataSourceType, item.DisplayExpr, item.ValueExpr, item.LookupQuery) + : null, + }, autoSave: true); + + await CreateLangKeyAsync(item.CaptionName, item.EnglishCaption, item.TurkishCaption); + } + } + } + + private async Task CreateLangKeyAsync(string key, string textEn, string textTr) + { + if (string.IsNullOrWhiteSpace(key)) return; + + var langKey = await _repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == _appName && a.Key == key) + ?? await _repoLangKey.InsertAsync(new LanguageKey { ResourceName = _appName, Key = key }, autoSave: true); + + var existingTexts = await _repoLangText.GetListAsync(a => a.ResourceName == _appName && a.Key == langKey.Key); + + var existingEn = existingTexts.FirstOrDefault(a => a.CultureName == _cultureNameDefault); + if (existingEn == null) + { + await _repoLangText.InsertAsync(new LanguageText + { + ResourceName = _appName, + Key = langKey.Key, + CultureName = _cultureNameDefault, + Value = textEn ?? key + }, autoSave: true); + } + + var existingTr = existingTexts.FirstOrDefault(a => a.CultureName == LanguageCodes.Tr); + if (existingTr == null) + { + await _repoLangText.InsertAsync(new LanguageText + { + ResourceName = _appName, + Key = langKey.Key, + CultureName = LanguageCodes.Tr, + Value = textTr ?? key + }, autoSave: true); + } + } +} + + diff --git a/api/src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj b/api/src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj index 0f21005..e0b243c 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj +++ b/api/src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj @@ -77,6 +77,10 @@ PreserveNewest Always + + PreserveNewest + Always + diff --git a/ui/src/proxy/admin/list-form/models.ts b/ui/src/proxy/admin/list-form/models.ts index e2a155f..a477419 100644 --- a/ui/src/proxy/admin/list-form/models.ts +++ b/ui/src/proxy/admin/list-form/models.ts @@ -72,6 +72,14 @@ export interface ListFormWizardDto { groups?: ListFormWizardColumnGroupDto[] } +export interface WizardFileInfoDto { + fileName: string + wizardName: string + listFormCode: string + createdAt: string + hasInsertedRecords: boolean +} + export interface ListFormJsonRowDto { id?: string index: number diff --git a/ui/src/services/admin/list-form.service.ts b/ui/src/services/admin/list-form.service.ts index 7310f18..c11f23d 100644 --- a/ui/src/services/admin/list-form.service.ts +++ b/ui/src/services/admin/list-form.service.ts @@ -1,14 +1,7 @@ -import { ListFormJsonRowDto, ListFormWizardDto } from '../../proxy/admin/list-form/models' +import { ListFormJsonRowDto } from '../../proxy/admin/list-form/models' import { FieldsDefaultValueDto, GridOptionsEditDto } from '../../proxy/form/models' import apiService from '../api.service' -export const postListFormWizard = (input: ListFormWizardDto) => - apiService.fetchData({ - method: 'POST', - url: `/api/app/list-form-wizard`, - data: input, - }) - export const getListFormByCode = (listFormCode: string) => apiService.fetchData({ method: 'GET', diff --git a/ui/src/services/wizard.service.ts b/ui/src/services/wizard.service.ts new file mode 100644 index 0000000..833b6cb --- /dev/null +++ b/ui/src/services/wizard.service.ts @@ -0,0 +1,22 @@ +import { ListFormWizardDto, WizardFileInfoDto } from '../proxy/admin/list-form/models' +import apiService from './api.service' + +export const postListFormWizard = (input: ListFormWizardDto) => + apiService.fetchData({ + method: 'POST', + url: `/api/app/list-form-wizard`, + data: input, + }) + +export const getWizardFiles = () => + apiService.fetchData({ + method: 'GET', + url: `/api/app/list-form-wizard/files`, + }) + +export const deleteWizardFile = (fileName: string) => + apiService.fetchData({ + method: 'DELETE', + url: `/api/app/list-form-wizard/file`, + params: { fileName }, + }) \ No newline at end of file diff --git a/ui/src/views/admin/listForm/Wizard.tsx b/ui/src/views/admin/listForm/wizard/Wizard.tsx similarity index 99% rename from ui/src/views/admin/listForm/Wizard.tsx rename to ui/src/views/admin/listForm/wizard/Wizard.tsx index f2c3563..b0f6806 100644 --- a/ui/src/views/admin/listForm/Wizard.tsx +++ b/ui/src/views/admin/listForm/wizard/Wizard.tsx @@ -11,7 +11,6 @@ import * as Yup from 'yup' import { getMenus } from '@/services/menu.service' import { getPermissions } from '@/services/identity.service' import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models' -import { postListFormWizard } from '@/services/admin/list-form.service' import { getDataSources } from '@/services/data-source.service' import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import type { SqlObjectExplorerDto, DatabaseColumnDto } from '@/proxy/sql-query-manager/models' @@ -27,8 +26,9 @@ import WizardStep2 from './WizardStep2' import WizardStep3, { WizardGroup } from './WizardStep3' import WizardStep4 from './WizardStep4' import { Container } from '@/components/shared' -import { sqlDataTypeToDbType } from './edit/options' +import { sqlDataTypeToDbType } from '../edit/options' import { useStoreActions } from '@/store/store' +import { postListFormWizard } from '@/services/wizard.service' // ─── Formik initial values & validation ────────────────────────────────────── const initialValues: ListFormWizardDto = { diff --git a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx new file mode 100644 index 0000000..2454ce9 --- /dev/null +++ b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx @@ -0,0 +1,217 @@ +import { useState, useEffect, useCallback } from 'react' +import { useNavigate } from 'react-router-dom' +import classNames from 'classnames' +import { Button, Notification, toast } from '@/components/ui' +import Container from '@/components/shared/Container' +import { WizardFileInfoDto } from '@/proxy/admin/list-form/models' +import { FaTrash, FaSync, FaDatabase, FaPlus, FaExclamationTriangle } from 'react-icons/fa' +import { deleteWizardFile, getWizardFiles } from '@/services/wizard.service' +import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon' +import { useLocalization } from '@/utils/hooks/useLocalization' +import { useStoreState } from '@/store/store' +import { ROUTES_ENUM } from '@/routes/route.constant' + +// Timestamp formatı: "202605021730" → "2026-05-02 17:30" +const formatTimestamp = (raw: string): string => { + if (raw.length < 12) return raw + const y = raw.slice(0, 4) + const mo = raw.slice(4, 6) + const d = raw.slice(6, 8) + const h = raw.slice(8, 10) + const mi = raw.slice(10, 12) + return `${y}-${mo}-${d} ${h}:${mi}` +} + +interface ConfirmState { + fileName: string + wizardName: string +} + +const WizardFileManager = () => { + const navigate = useNavigate() + const { translate } = useLocalization() + const mode = useStoreState((state) => state.theme.mode) + const MenuIcon = useCurrentMenuIcon('w-5 h-5') + + const [files, setFiles] = useState([]) + const [loading, setLoading] = useState(false) + const [deletingFile, setDeletingFile] = useState(null) + const [confirm, setConfirm] = useState(null) + + const loadFiles = useCallback(async () => { + setLoading(true) + try { + const res = await getWizardFiles() + setFiles(res.data ?? []) + } catch { + toast.push( + Wizard dosyaları yüklenemedi., + { placement: 'top-end' }, + ) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + loadFiles() + }, [loadFiles]) + + const handleDeleteConfirm = async () => { + if (!confirm) return + setDeletingFile(confirm.fileName) + setConfirm(null) + try { + await deleteWizardFile(confirm.fileName) + toast.push( + + {confirm.wizardName} silindi. + , + { placement: 'top-end' }, + ) + await loadFiles() + } catch (err: any) { + toast.push( + + Silme başarısız: {err?.message ?? 'Bilinmeyen hata'} + , + { placement: 'top-end' }, + ) + } finally { + setDeletingFile(null) + } + } + + return ( + + {/* ── Header ─────────────────────────────────────────────── */} +
+ {MenuIcon} +

+ {translate('::App.Listforms.WizardManager') || 'Wizard Seed Dosyaları'} +

+ +
+ + +
+
+ +
+ + {files.length === 0 && !loading && ( +

+ Henüz kaydedilmiş wizard dosyası yok. +

+ )} + + {loading && ( +

Yükleniyor...

+ )} + +
+ {files.map((f) => ( +
+
+ +
+
+ {f.wizardName || f.fileName} +
+
+ {formatTimestamp(f.createdAt)} + {f.listFormCode} +
+
+
+ +
+ {!f.hasInsertedRecords && ( + + + + )} + +
+
+ ))} +
+ + {/* Confirm Dialog */} + {confirm && ( +
+
+
+ +
+

Wizard Sil

+

+ {confirm.wizardName} wizard'ı ve buna ait tüm veritabanı + kayıtları (izin, menü, dil, listform) silinecek. Bu işlem geri alınamaz. +

+
+
+
+ + +
+
+
+ )} +
+
+ ) +} + +export default WizardFileManager diff --git a/ui/src/views/admin/listForm/WizardStep1.tsx b/ui/src/views/admin/listForm/wizard/WizardStep1.tsx similarity index 100% rename from ui/src/views/admin/listForm/WizardStep1.tsx rename to ui/src/views/admin/listForm/wizard/WizardStep1.tsx diff --git a/ui/src/views/admin/listForm/WizardStep2.tsx b/ui/src/views/admin/listForm/wizard/WizardStep2.tsx similarity index 99% rename from ui/src/views/admin/listForm/WizardStep2.tsx rename to ui/src/views/admin/listForm/wizard/WizardStep2.tsx index 2438051..2194357 100644 --- a/ui/src/views/admin/listForm/WizardStep2.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardStep2.tsx @@ -3,15 +3,10 @@ import { ListFormWizardDto } from '@/proxy/admin/list-form/models' import { SelectCommandTypeEnum } from '@/proxy/form/models' import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models' import { SelectBoxOption } from '@/types/shared' -import { - dbSourceTypeOptions, - listFormDefaultLayoutOptions, - selectCommandTypeOptions, - sqlDataTypeToDbType, -} from './edit/options' import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik' import CreatableSelect from 'react-select/creatable' import { FaArrowLeft, FaArrowRight } from 'react-icons/fa' +import { dbSourceTypeOptions, listFormDefaultLayoutOptions, selectCommandTypeOptions, sqlDataTypeToDbType } from '../edit/options' // ─── Props ──────────────────────────────────────────────────────────────────── diff --git a/ui/src/views/admin/listForm/WizardStep3.tsx b/ui/src/views/admin/listForm/wizard/WizardStep3.tsx similarity index 100% rename from ui/src/views/admin/listForm/WizardStep3.tsx rename to ui/src/views/admin/listForm/wizard/WizardStep3.tsx diff --git a/ui/src/views/admin/listForm/WizardStep4.tsx b/ui/src/views/admin/listForm/wizard/WizardStep4.tsx similarity index 99% rename from ui/src/views/admin/listForm/WizardStep4.tsx rename to ui/src/views/admin/listForm/wizard/WizardStep4.tsx index 8f9af1f..269c617 100644 --- a/ui/src/views/admin/listForm/WizardStep4.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardStep4.tsx @@ -1,7 +1,6 @@ import { Button } from '@/components/ui' import type { ListFormWizardDto } from '@/proxy/admin/list-form/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' -import type { WizardGroup } from './WizardStep3' import { useState } from 'react' import { FaArrowLeft, @@ -13,7 +12,8 @@ import { FaRocket, FaSpinner, } from 'react-icons/fa' -import { dbSourceTypeOptions, selectCommandTypeOptions } from './edit/options' +import { WizardGroup } from './WizardStep3' +import { dbSourceTypeOptions, selectCommandTypeOptions } from '../edit/options' // ─── Types ──────────────────────────────────────────────────────────────────── diff --git a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx index 9c7e627..74f396b 100644 --- a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx +++ b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx @@ -27,7 +27,7 @@ import { MenuTreeNode, buildMenuTree, filterNonLinkNodes, -} from '@/views/admin/listForm/WizardStep1' +} from '@/views/admin/listForm/wizard/WizardStep1' import { MenuAddDialog } from '../shared/MenuAddDialog' // ─── Types ────────────────────────────────────────────────────────────────────