ListForm Wizard Manager

This commit is contained in:
Sedat Öztürk 2026-05-02 20:08:06 +03:00
parent 503c45282b
commit a72faa083c
18 changed files with 999 additions and 59 deletions

View file

@ -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<List<WizardFileInfoDto>> GetFiles();
Task DeleteFile(string fileName);
}

View file

@ -0,0 +1,57 @@
using System.Collections.Generic;
namespace Sozsoft.Platform.ListForms;
/// <summary>
/// Wizard seed dosyası formatı.
/// WizardAppService tarafından Seeds/WizardData/{wizardName}.json olarak kaydedilir.
/// WizardDataSeeder (DbMigrator) bu dosyaları okuyarak veritabanını yeniden oluşturur.
/// </summary>
public class WizardSeedFileDto
{
public ListFormWizardDto Wizard { get; set; }
/// <summary>Tabloda IsDeleted alanı olup olmadığını belirtir (soft delete desteği).</summary>
public bool IsDeletedField { get; set; }
/// <summary>Tabloda CreatorId alanı olup olmadığını belirtir (audit alanı).</summary>
public bool IsCreatedField { get; set; }
/// <summary>
/// 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.
/// </summary>
public WizardInsertedRecordsDto InsertedRecords { get; set; } = new();
}
/// <summary>
/// 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.
/// </summary>
public class WizardInsertedRecordsDto
{
/// <summary>Bu Wizard tarafından oluşturulan yeni LanguageKey değerleri (Key alanı).</summary>
public List<string> LanguageKeys { get; set; } = [];
/// <summary>Bu Wizard tarafından oluşturulan yeni PermissionGroup isimleri.</summary>
public List<string> PermissionGroupNames { get; set; } = [];
/// <summary>Bu Wizard tarafından oluşturulan yeni Permission isimleri.</summary>
public List<string> PermissionNames { get; set; } = [];
/// <summary>Bu Wizard tarafından oluşturulan yeni Menu kodları.</summary>
public List<string> MenuCodes { get; set; } = [];
/// <summary>Bu Wizard tarafından oluşturulan yeni DataSource kodları.</summary>
public List<string> DataSourceCodes { get; set; } = [];
}
/// <summary>Wizard seed dosyası özet bilgisi (yönetim listesi için).</summary>
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; }
}

View file

@ -20,7 +20,7 @@ using static Sozsoft.Platform.PlatformConsts;
namespace Sozsoft.Platform.ListForms.ImportManager;
[Authorize()]
[Authorize]
public class ListFormImportAppService : PlatformAppService, IImportAppService
{
private readonly IRepository<ListFormImport, Guid> _importSessionRepository;

View file

@ -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<ListForm, Guid> repoListForm,
IRepository<ListFormField, Guid> repoListFormField,
@ -31,7 +32,7 @@ public class ListFormWizardAppService(
IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup,
IRepository<Menu, Guid> repoMenu,
IPermissionGrantRepository permissionGrantRepository,
IConfiguration configuration,
IHostEnvironment hostEnvironment,
LanguageTextAppService languageTextAppService,
IDynamicDataManager dynamicDataManager
) : PlatformAppService(), IListFormWizardAppService
@ -45,7 +46,7 @@ public class ListFormWizardAppService(
private readonly IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup = repoPermGroup;
private readonly IRepository<Menu, Guid> 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);
}
/// <summary>
/// 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.
/// </summary>
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<List<WizardFileInfoDto>> GetFiles()
{
var outputPath = ResolveWizardSeedOutputPath();
var result = new List<WizardFileInfoDto>();
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<WizardSeedFileDto>(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<WizardSeedFileDto>(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);
}
}
/// <summary>
/// DbMigrator projesinin Seeds/WizardData dizinini ContentRootPath'ten yukarı traversal ile bulur.
/// Tüm işletim sistemlerinde Path.Combine kullanır, separator karakteri içermez.
/// </summary>
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<HashSet<string>> GetTableColumnNamesAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand)
@ -312,23 +551,27 @@ public class ListFormWizardAppService(
}
}
private async Task<LanguageKey> CreateLangKey(string key, string textEn, string textTr)
private async Task<LanguageKey> 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;
}
}

View file

@ -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",

View file

@ -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

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
{
private readonly IRepository<LanguageKey, Guid> _repoLangKey;
private readonly IRepository<LanguageText, Guid> _repoLangText;
private readonly IRepository<PermissionGroupDefinitionRecord, Guid> _repoPermGroup;
private readonly IRepository<PermissionDefinitionRecord, Guid> _repoPerm;
private readonly IPermissionGrantRepository _permissionGrantRepository;
private readonly IRepository<Menu, Guid> _repoMenu;
private readonly IRepository<DataSource, Guid> _repoDataSource;
private readonly IRepository<ListForm, Guid> _repoListForm;
private readonly IRepository<ListFormField, Guid> _repoListFormField;
private readonly ILogger<WizardDataSeeder> _logger;
private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage;
private readonly string _appName = PlatformConsts.AppName;
public WizardDataSeeder(
IRepository<LanguageKey, Guid> repoLangKey,
IRepository<LanguageText, Guid> repoLangText,
IRepository<PermissionGroupDefinitionRecord, Guid> repoPermGroup,
IRepository<PermissionDefinitionRecord, Guid> repoPerm,
IPermissionGrantRepository permissionGrantRepository,
IRepository<Menu, Guid> repoMenu,
IRepository<DataSource, Guid> repoDataSource,
IRepository<ListForm, Guid> repoListForm,
IRepository<ListFormField, Guid> repoListFormField,
ILogger<WizardDataSeeder> 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<WizardSeedFileDto>(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);
}
}
}

View file

@ -77,6 +77,10 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\WizardData\*.json">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

View file

@ -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

View file

@ -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<GridOptionsEditDto>({
method: 'GET',

View file

@ -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<WizardFileInfoDto[]>({
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 },
})

View file

@ -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 = {

View file

@ -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<WizardFileInfoDto[]>([])
const [loading, setLoading] = useState(false)
const [deletingFile, setDeletingFile] = useState<string | null>(null)
const [confirm, setConfirm] = useState<ConfirmState | null>(null)
const loadFiles = useCallback(async () => {
setLoading(true)
try {
const res = await getWizardFiles()
setFiles(res.data ?? [])
} catch {
toast.push(
<Notification type="danger">Wizard dosyaları yüklenemedi.</Notification>,
{ 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(
<Notification type="success" duration={3000}>
<strong>{confirm.wizardName}</strong> silindi.
</Notification>,
{ placement: 'top-end' },
)
await loadFiles()
} catch (err: any) {
toast.push(
<Notification type="danger">
Silme başarısız: {err?.message ?? 'Bilinmeyen hata'}
</Notification>,
{ placement: 'top-end' },
)
} finally {
setDeletingFile(null)
}
}
return (
<Container>
{/* ── Header ─────────────────────────────────────────────── */}
<div
className={classNames(
'flex items-center gap-2 pb-1 border-b',
mode === 'light' ? 'border-gray-200' : 'border-neutral-700',
)}
>
{MenuIcon}
<h4 className="text-sm font-medium">
{translate('::App.Listforms.WizardManager') || 'Wizard Seed Dosyaları'}
</h4>
<div className="flex gap-1 ml-auto">
<Button
size="sm"
variant="default"
title={translate('::App.Platform.Refresh') || 'Yenile'}
loading={loading}
onClick={loadFiles}
>
<FaSync />
</Button>
<Button
size="sm"
variant="solid"
onClick={() => navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard)}
className="flex items-center"
>
<FaPlus className="mr-1" />
{translate('::ListForms.ListForm.AddNewRecord') || 'Add New Record'}
</Button>
</div>
</div>
<div className="mt-4">
{files.length === 0 && !loading && (
<p className="text-xs text-gray-400 text-center py-4">
Henüz kaydedilmiş wizard dosyası yok.
</p>
)}
{loading && (
<p className="text-xs text-gray-400 text-center py-4 animate-pulse">Yükleniyor...</p>
)}
<div className="space-y-2">
{files.map((f) => (
<div
key={f.fileName}
className="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
>
<div className="flex items-center gap-3 min-w-0">
<FaDatabase className="text-indigo-400 shrink-0" />
<div className="min-w-0">
<div className="font-medium text-sm text-gray-800 dark:text-gray-200 truncate">
{f.wizardName || f.fileName}
</div>
<div className="text-xs text-gray-400 flex gap-3 mt-0.5">
<span>{formatTimestamp(f.createdAt)}</span>
<span className="truncate">{f.listFormCode}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2 shrink-0 ml-3">
{!f.hasInsertedRecords && (
<span
title="Bu dosyada izlenen kayıt bilgisi yok. Eski format olabilir."
className="text-yellow-500 text-xs flex items-center gap-1"
>
<FaExclamationTriangle />
</span>
)}
<Button
size="xs"
variant="plain"
className="text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
type="button"
loading={deletingFile === f.fileName}
onClick={() => setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })}
>
<FaTrash />
</Button>
</div>
</div>
))}
</div>
{/* Confirm Dialog */}
{confirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-xl p-6 max-w-sm w-full mx-4">
<div className="flex items-center gap-3 mb-4">
<FaExclamationTriangle className="text-red-500 text-xl shrink-0" />
<div>
<p className="font-semibold text-gray-800 dark:text-gray-200">Wizard Sil</p>
<p className="text-sm text-gray-500 mt-1">
<strong>{confirm.wizardName}</strong> wizard'ı ve buna ait tüm veritabanı
kayıtları (izin, menü, dil, listform) silinecek. Bu işlem geri alınamaz.
</p>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button
size="sm"
variant="plain"
type="button"
onClick={() => setConfirm(null)}
>
İptal
</Button>
<Button
size="sm"
variant="solid"
color="red"
type="button"
onClick={handleDeleteConfirm}
>
Evet, Sil
</Button>
</div>
</div>
</div>
)}
</div>
</Container>
)
}
export default WizardFileManager

View file

@ -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 ────────────────────────────────────────────────────────────────────

View file

@ -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 ────────────────────────────────────────────────────────────────────

View file

@ -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 ────────────────────────────────────────────────────────────────────