Claude güncellemesi WizardManager güncellemesi
This commit is contained in:
parent
5626c27a90
commit
db24baed91
15 changed files with 497 additions and 397 deletions
|
|
@ -1,38 +1,18 @@
|
||||||
using Sozsoft.Platform.Localization;
|
|
||||||
using Volo.Abp.Authorization.Permissions;
|
using Volo.Abp.Authorization.Permissions;
|
||||||
using Volo.Abp.Localization;
|
|
||||||
using Volo.Abp.MultiTenancy;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Permissions;
|
namespace Sozsoft.Platform.Permissions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uygulama yetkileri kod içinde tanımlanmaz; <c>Seeds/PermissionsData.json</c>
|
||||||
|
/// dosyasından <c>PermissionDataSeeder</c> ile veritabanına seed edilir ve
|
||||||
|
/// oradan dinamik olarak yönetilir. Bu sağlayıcı yalnızca kullanılmayan
|
||||||
|
/// ABP modül gruplarını kaldırmak için vardır.
|
||||||
|
/// </summary>
|
||||||
public class PlatformPermissionDefinitionProvider : PermissionDefinitionProvider
|
public class PlatformPermissionDefinitionProvider : PermissionDefinitionProvider
|
||||||
{
|
{
|
||||||
public override void Define(IPermissionDefinitionContext context)
|
public override void Define(IPermissionDefinitionContext context)
|
||||||
{
|
{
|
||||||
PermissionRemoveGroup(context);
|
PermissionRemoveGroup(context);
|
||||||
|
|
||||||
DefineSetupPermissions(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kurulum / veritabanı bakım yetkileri. <see cref="MultiTenancySides.Host"/> ile
|
|
||||||
/// işaretlendiği için tenant kullanıcılarına hiç görünmez ve verilemez.
|
|
||||||
/// </summary>
|
|
||||||
private void DefineSetupPermissions(IPermissionDefinitionContext context)
|
|
||||||
{
|
|
||||||
var setupGroup = context.AddGroup(
|
|
||||||
PlatformPermissions.GroupName,
|
|
||||||
L("Permission:Setup"));
|
|
||||||
|
|
||||||
setupGroup.AddPermission(
|
|
||||||
PlatformPermissions.Setup.Migrate,
|
|
||||||
L("Permission:Setup.Migrate"),
|
|
||||||
MultiTenancySides.Host);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocalizableString L(string name)
|
|
||||||
{
|
|
||||||
return LocalizableString.Create<PlatformResource>(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PermissionRemoveGroup(IPermissionDefinitionContext context)
|
private void PermissionRemoveGroup(IPermissionDefinitionContext context)
|
||||||
|
|
@ -42,6 +22,4 @@ public class PlatformPermissionDefinitionProvider : PermissionDefinitionProvider
|
||||||
context.RemoveGroup("FeatureManagement");
|
context.RemoveGroup("FeatureManagement");
|
||||||
context.RemoveGroup("AbpIdentity");
|
context.RemoveGroup("AbpIdentity");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using Sozsoft.Languages.Entities;
|
||||||
using Sozsoft.Languages.Languages;
|
using Sozsoft.Languages.Languages;
|
||||||
using Sozsoft.Platform.Entities;
|
using Sozsoft.Platform.Entities;
|
||||||
using Sozsoft.Platform.Enums;
|
using Sozsoft.Platform.Enums;
|
||||||
|
using Volo.Abp;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
using Volo.Abp.MultiTenancy;
|
using Volo.Abp.MultiTenancy;
|
||||||
using Volo.Abp.PermissionManagement;
|
using Volo.Abp.PermissionManagement;
|
||||||
|
|
@ -17,10 +18,15 @@ using Sozsoft.Languages;
|
||||||
using Sozsoft.Platform.DynamicData;
|
using Sozsoft.Platform.DynamicData;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static Sozsoft.Platform.PlatformConsts;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.ListForms;
|
namespace Sozsoft.Platform.ListForms;
|
||||||
|
|
||||||
[Authorize]
|
/// <summary>
|
||||||
|
/// Wizard, global menu/permission/language kayitlari ve seed dosyalari uretir.
|
||||||
|
/// Bu nedenle sadece host tarafinda tanimli olan <c>App.Listforms.Wizard</c> yetkisi ile calisir.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(AppCodes.Listforms.Wizard)]
|
||||||
public class ListFormWizardAppService(
|
public class ListFormWizardAppService(
|
||||||
IRepository<ListForm, Guid> repoListForm,
|
IRepository<ListForm, Guid> repoListForm,
|
||||||
IRepository<ListFormField, Guid> repoListFormField,
|
IRepository<ListFormField, Guid> repoListFormField,
|
||||||
|
|
@ -54,25 +60,31 @@ public class ListFormWizardAppService(
|
||||||
private readonly ILogger<ListFormWizardAppService> logger = logger;
|
private readonly ILogger<ListFormWizardAppService> logger = logger;
|
||||||
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
|
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions SeedJsonReadOptions = new() { PropertyNameCaseInsensitive = true };
|
||||||
|
private static readonly JsonSerializerOptions SeedJsonWriteOptions = new() { WriteIndented = true };
|
||||||
|
|
||||||
[UnitOfWork]
|
[UnitOfWork]
|
||||||
public async Task Create(ListFormWizardDto input)
|
public async Task Create(ListFormWizardDto input)
|
||||||
{
|
{
|
||||||
var wizardName = input.WizardName.Trim();
|
Check.NotNull(input, nameof(input));
|
||||||
|
|
||||||
|
var wizardName = input.WizardName?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(wizardName))
|
||||||
|
throw new UserFriendlyException("Wizard adı zorunludur.");
|
||||||
|
|
||||||
var code = string.IsNullOrWhiteSpace(input.MenuCode)
|
var code = string.IsNullOrWhiteSpace(input.MenuCode)
|
||||||
? WizardConsts.WizardKey(wizardName)
|
? WizardConsts.WizardKey(wizardName)
|
||||||
: input.MenuCode.Trim();
|
: input.MenuCode.Trim();
|
||||||
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
|
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
|
||||||
? code
|
? code
|
||||||
: input.ListFormCode.Trim();
|
: input.ListFormCode.Trim();
|
||||||
|
// Aşağıdaki tüm kayıtlar normalize edilmiş kodlara göre üretilir.
|
||||||
|
input.ListFormCode = listFormCode;
|
||||||
|
input.MenuCode = code;
|
||||||
|
|
||||||
var titleLangKey = $"{listFormCode}.Title";
|
var titleLangKey = $"{listFormCode}.Title";
|
||||||
var nameLangKey = code;
|
var nameLangKey = code;
|
||||||
var descLangKey = $"{listFormCode}.Desc";
|
var descLangKey = $"{listFormCode}.Desc";
|
||||||
var permCreateName = $"{code}.Create";
|
|
||||||
var permUpdateName = $"{code}.Update";
|
|
||||||
var permDeleteName = $"{code}.Delete";
|
|
||||||
var permExportName = $"{code}.Export";
|
|
||||||
var permImportName = $"{code}.Import";
|
|
||||||
var permNoteName = $"{code}.Note";
|
|
||||||
|
|
||||||
// Eklenen kayıtları takip et (silme işleminde kullanılır)
|
// Eklenen kayıtları takip et (silme işleminde kullanılır)
|
||||||
var inserted = new WizardInsertedRecordsDto();
|
var inserted = new WizardInsertedRecordsDto();
|
||||||
|
|
@ -96,66 +108,49 @@ public class ListFormWizardAppService(
|
||||||
|
|
||||||
// Permission'ları tek seferde kontrol et ve oluştur
|
// Permission'ları tek seferde kontrol et ve oluştur
|
||||||
var existingPerms = await repoPerm.GetListAsync(a => a.GroupName == groupName);
|
var existingPerms = await repoPerm.GetListAsync(a => a.GroupName == groupName);
|
||||||
|
|
||||||
var permRead = existingPerms.FirstOrDefault(a => a.Name == code);
|
var permRead = existingPerms.FirstOrDefault(a => a.Name == code);
|
||||||
if (permRead == null)
|
if (permRead == null)
|
||||||
{
|
{
|
||||||
permRead = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both), autoSave: false);
|
permRead = await repoPerm.InsertAsync(
|
||||||
|
new PermissionDefinitionRecord(GuidGenerator.Create(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both),
|
||||||
|
autoSave: false);
|
||||||
inserted.PermissionNames.Add(permRead.Name);
|
inserted.PermissionNames.Add(permRead.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var permCreate = existingPerms.FirstOrDefault(a => a.Name == permCreateName);
|
// Read altındaki alt yetkiler: (suffix, dil anahtarı)
|
||||||
if (permCreate == null)
|
var childPermissions = new[]
|
||||||
{
|
{
|
||||||
permCreate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: false);
|
("Create", WizardConsts.LangKeyCreate),
|
||||||
inserted.PermissionNames.Add(permCreate.Name);
|
("Update", WizardConsts.LangKeyUpdate),
|
||||||
}
|
("Delete", WizardConsts.LangKeyDelete),
|
||||||
|
("Export", WizardConsts.LangKeyExport),
|
||||||
|
("Import", WizardConsts.LangKeyImport),
|
||||||
|
("Note", WizardConsts.LangKeyNote),
|
||||||
|
};
|
||||||
|
|
||||||
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == permUpdateName);
|
var permissionNames = new List<string> { permRead.Name };
|
||||||
if (permUpdate == null)
|
foreach (var (suffix, langKey) in childPermissions)
|
||||||
{
|
{
|
||||||
permUpdate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: false);
|
var permissionName = $"{code}.{suffix}";
|
||||||
inserted.PermissionNames.Add(permUpdate.Name);
|
permissionNames.Add(permissionName);
|
||||||
}
|
|
||||||
|
|
||||||
var permDelete = existingPerms.FirstOrDefault(a => a.Name == permDeleteName);
|
if (existingPerms.Any(a => a.Name == permissionName))
|
||||||
if (permDelete == null)
|
continue;
|
||||||
{
|
|
||||||
permDelete = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: false);
|
|
||||||
inserted.PermissionNames.Add(permDelete.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
var permExport = existingPerms.FirstOrDefault(a => a.Name == permExportName);
|
await repoPerm.InsertAsync(
|
||||||
if (permExport == null)
|
new PermissionDefinitionRecord(GuidGenerator.Create(), groupName, permissionName, permRead.Name, langKey, true, MultiTenancySides.Both),
|
||||||
{
|
autoSave: false);
|
||||||
permExport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: false);
|
inserted.PermissionNames.Add(permissionName);
|
||||||
inserted.PermissionNames.Add(permExport.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
var permImport = existingPerms.FirstOrDefault(a => a.Name == permImportName);
|
|
||||||
if (permImport == null)
|
|
||||||
{
|
|
||||||
permImport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: false);
|
|
||||||
inserted.PermissionNames.Add(permImport.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
var permNote = existingPerms.FirstOrDefault(a => a.Name == permNoteName);
|
|
||||||
if (permNote == null)
|
|
||||||
{
|
|
||||||
permNote = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: false);
|
|
||||||
inserted.PermissionNames.Add(permNote.Name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permission Grants - Bulk Insert (only missing ones)
|
// Permission Grants - Bulk Insert (only missing ones)
|
||||||
var existingGrants = await permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName);
|
var existingGrants = await permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName);
|
||||||
var existingGrantNames = existingGrants.Select(g => g.Name).ToHashSet();
|
var existingGrantNames = existingGrants.Select(g => g.Name).ToHashSet();
|
||||||
|
|
||||||
var grantsToInsert = new[]
|
var grantsToInsert = permissionNames
|
||||||
{
|
|
||||||
permRead.Name, permCreate.Name, permUpdate.Name,
|
|
||||||
permDelete.Name, permExport.Name, permImport.Name, permNote.Name
|
|
||||||
}
|
|
||||||
.Where(name => !existingGrantNames.Contains(name))
|
.Where(name => !existingGrantNames.Contains(name))
|
||||||
.Select(name => new PermissionGrant(Guid.NewGuid(), name, "R", PlatformConsts.AbpIdentity.User.AdminRoleName))
|
.Select(name => new PermissionGrant(GuidGenerator.Create(), name, "R", PlatformConsts.AbpIdentity.User.AdminRoleName))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (grantsToInsert.Count > 0)
|
if (grantsToInsert.Count > 0)
|
||||||
|
|
@ -227,26 +222,32 @@ public class ListFormWizardAppService(
|
||||||
var dataSource = await AsyncExecuter.FirstOrDefaultAsync(dataSourceQueryable.Where(a => a.Code == input.DataSourceCode));
|
var dataSource = await AsyncExecuter.FirstOrDefaultAsync(dataSourceQueryable.Where(a => a.Code == input.DataSourceCode));
|
||||||
if (dataSource is null)
|
if (dataSource is null)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input.DataSourceConnectionString))
|
||||||
|
throw new UserFriendlyException($"'{input.DataSourceCode}' veri kaynağı bulunamadı. Yeni veri kaynağı için bağlantı cümlesi zorunludur.");
|
||||||
|
|
||||||
await repoDataSource.InsertAsync(new DataSource
|
await repoDataSource.InsertAsync(new DataSource
|
||||||
{
|
{
|
||||||
Code = input.DataSourceCode,
|
Code = input.DataSourceCode,
|
||||||
DataSourceType = input.DataSourceConnectionString.IndexOf("Server") >= 0 ? DataSourceTypeEnum.Mssql : DataSourceTypeEnum.Postgresql,
|
DataSourceType = input.DataSourceConnectionString.Contains("Server", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? DataSourceTypeEnum.Mssql
|
||||||
|
: DataSourceTypeEnum.Postgresql,
|
||||||
ConnectionString = input.DataSourceConnectionString
|
ConnectionString = input.DataSourceConnectionString
|
||||||
}, autoSave: false);
|
}, autoSave: false);
|
||||||
inserted.DataSourceCodes.Add(input.DataSourceCode);
|
inserted.DataSourceCodes.Add(input.DataSourceCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build EditingFormJson from wizard groups
|
// Build EditingFormJson from wizard groups (sadece editing form alanı olan gruplar)
|
||||||
var editingFormDtos = input.Groups
|
var editingFormDtos = input.Groups
|
||||||
.Select((g, gi) => new EditingFormDto
|
.Select((g, gi) => new { Group = g, Order = gi + 1 })
|
||||||
|
.Where(x => x.Group.Items.Any(i => i.IncludeInEditingForm && i.FieldName != input.KeyFieldName))
|
||||||
|
.Select(x => new EditingFormDto
|
||||||
{
|
{
|
||||||
Order = gi + 1,
|
Order = x.Order,
|
||||||
Caption = g.Caption,
|
Caption = x.Group.Caption,
|
||||||
ColCount = g.ColCount,
|
ColCount = x.Group.ColCount,
|
||||||
ColSpan = g.ColCount,
|
ColSpan = x.Group.ColCount,
|
||||||
ItemType = "group"
|
ItemType = "group"
|
||||||
})
|
})
|
||||||
.Where((_, index) => input.Groups[index].Items.Any(i => i.IncludeInEditingForm && i.FieldName != input.KeyFieldName))
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
//ListForm - varsa sil, yeniden ekle
|
//ListForm - varsa sil, yeniden ekle
|
||||||
|
|
@ -373,7 +374,9 @@ public class ListFormWizardAppService(
|
||||||
ColumnCustomizationJson = WizardConsts.DefaultColumnCustomizationJson,
|
ColumnCustomizationJson = WizardConsts.DefaultColumnCustomizationJson,
|
||||||
ColumnFilterJson = WizardConsts.DefaultColumnFilteringJson,
|
ColumnFilterJson = WizardConsts.DefaultColumnFilteringJson,
|
||||||
PivotSettingsJson = WizardConsts.DefaultPivotSettingsJson,
|
PivotSettingsJson = WizardConsts.DefaultPivotSettingsJson,
|
||||||
LookupJson = item.LookupQuery.Length > 0 ? WizardConsts.DefaultLookupJson(item.LookupDataSourceType, item.DisplayExpr, item.ValueExpr, item.LookupQuery) : null,
|
LookupJson = !string.IsNullOrWhiteSpace(item.LookupQuery)
|
||||||
|
? WizardConsts.DefaultLookupJson(item.LookupDataSourceType, item.DisplayExpr, item.ValueExpr, item.LookupQuery)
|
||||||
|
: null,
|
||||||
}, autoSave: true);
|
}, autoSave: true);
|
||||||
|
|
||||||
await CreateLangKey(item.CaptionName, item.EnglishCaption, item.TurkishCaption, inserted);
|
await CreateLangKey(item.CaptionName, item.EnglishCaption, item.TurkishCaption, inserted);
|
||||||
|
|
@ -547,38 +550,40 @@ public class ListFormWizardAppService(
|
||||||
InsertedRecords = inserted
|
InsertedRecords = inserted
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(seedData, new JsonSerializerOptions { WriteIndented = true });
|
var json = JsonSerializer.Serialize(seedData, SeedJsonWriteOptions);
|
||||||
|
// Dosya adı kullanıcı girdisinden türetildiği için geçersiz karakterler ayıklanır.
|
||||||
var safeWizardName = string.Concat(input.WizardName.Trim().Split(Path.GetInvalidFileNameChars()));
|
var safeWizardName = string.Concat(input.WizardName.Trim().Split(Path.GetInvalidFileNameChars()));
|
||||||
var timestamp = DateTime.Now.ToString("yyyyMMddHHmm");
|
if (string.IsNullOrWhiteSpace(safeWizardName))
|
||||||
|
safeWizardName = "Wizard";
|
||||||
|
|
||||||
|
var timestamp = Clock.Now.ToString("yyyyMMddHHmmss");
|
||||||
var filePath = Path.Combine(outputPath, $"{timestamp}_{safeWizardName}.json");
|
var filePath = Path.Combine(outputPath, $"{timestamp}_{safeWizardName}.json");
|
||||||
await File.WriteAllTextAsync(filePath, json);
|
await File.WriteAllTextAsync(filePath, json);
|
||||||
|
|
||||||
logger.LogInformation($"Seed file saved: {filePath}");
|
logger.LogInformation("Seed file saved: {FilePath}", filePath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Dosya kaydetme hatası wizard işlemini engellemez
|
// Dosya kaydetme hatası wizard işlemini engellemez
|
||||||
logger.LogError(ex, $"Seed file could not be saved: {ex.Message}");
|
logger.LogError(ex, "Seed file could not be saved: {Message}", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<WizardFileInfoDto>> GetFiles()
|
public async Task<List<WizardFileInfoDto>> GetFiles()
|
||||||
{
|
{
|
||||||
var outputPath = ResolveWizardSeedOutputPath();
|
var outputPath = ResolveWizardSeedOutputPath();
|
||||||
var result = new List<WizardFileInfoDto>();
|
var result = new List<WizardFileInfoDto>();
|
||||||
|
|
||||||
if (!Directory.Exists(outputPath))
|
if (!Directory.Exists(outputPath))
|
||||||
return Task.FromResult(result);
|
return result;
|
||||||
|
|
||||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
foreach (var file in Directory.GetFiles(outputPath, "*.json").OrderBy(Path.GetFileName))
|
||||||
|
|
||||||
foreach (var file in Directory.GetFiles(outputPath, "*.json").OrderBy(f => Path.GetFileName(f)))
|
|
||||||
{
|
{
|
||||||
|
var fileName = Path.GetFileName(file);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var json = File.ReadAllText(file);
|
var json = await File.ReadAllTextAsync(file);
|
||||||
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, options);
|
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, SeedJsonReadOptions);
|
||||||
var fileName = Path.GetFileName(file);
|
|
||||||
result.Add(new WizardFileInfoDto
|
result.Add(new WizardFileInfoDto
|
||||||
{
|
{
|
||||||
FileName = fileName,
|
FileName = fileName,
|
||||||
|
|
@ -595,48 +600,32 @@ public class ListFormWizardAppService(
|
||||||
seed.InsertedRecords.DataSourceCodes.Count > 0)
|
seed.InsertedRecords.DataSourceCodes.Count > 0)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
result.Add(new WizardFileInfoDto { FileName = Path.GetFileName(file) });
|
logger.LogWarning(ex, "Wizard seed file could not be parsed: {FileName}", fileName);
|
||||||
|
result.Add(new WizardFileInfoDto { FileName = fileName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(result);
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WizardSeedFileDto> GetFile(string fileName)
|
public async Task<WizardSeedFileDto> GetFile(string fileName)
|
||||||
{
|
{
|
||||||
// Güvenlik: sadece dosya adı, path traversal yasak
|
var filePath = ResolveWizardSeedFilePath(fileName);
|
||||||
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 json = await File.ReadAllTextAsync(filePath);
|
||||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, SeedJsonReadOptions);
|
||||||
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, options);
|
return seed ?? throw new UserFriendlyException("Dosya okunamadı.");
|
||||||
return seed ?? throw new Volo.Abp.AbpException("Dosya okunamadı.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnitOfWork]
|
||||||
public async Task DeleteFile(string fileName)
|
public async Task DeleteFile(string fileName)
|
||||||
{
|
{
|
||||||
// Güvenlik: sadece dosya adı, path traversal yasak
|
var filePath = ResolveWizardSeedFilePath(fileName);
|
||||||
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 json = await File.ReadAllTextAsync(filePath);
|
||||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, SeedJsonReadOptions);
|
||||||
var seed = JsonSerializer.Deserialize<WizardSeedFileDto>(json, options);
|
|
||||||
|
|
||||||
if (seed != null)
|
if (seed != null)
|
||||||
await DeleteWizardDataAsync(seed);
|
await DeleteWizardDataAsync(seed);
|
||||||
|
|
@ -646,6 +635,37 @@ public class ListFormWizardAppService(
|
||||||
await _languageTextAppService.ClearRedisCacheAsync();
|
await _languageTextAppService.ClearRedisCacheAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kullanıcıdan gelen dosya adını doğrular ve seed dizini içinde kaldığını garanti eder.
|
||||||
|
/// Sadece düz dosya adı kabul edilir; dizin ayracı, üst dizin ifadesi ve `.json` dışındaki
|
||||||
|
/// uzantılar reddedilir. Ek olarak birleştirilen tam yol seed dizini ile karşılaştırılır.
|
||||||
|
/// </summary>
|
||||||
|
private string ResolveWizardSeedFilePath(string fileName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
|
throw new UserFriendlyException("Geçersiz dosya adı.");
|
||||||
|
|
||||||
|
// Path.GetFileName sadece son segmenti döndürür; farklıysa yol bileşeni var demektir.
|
||||||
|
if (!string.Equals(Path.GetFileName(fileName), fileName, StringComparison.Ordinal) ||
|
||||||
|
fileName.Contains("..", StringComparison.Ordinal) ||
|
||||||
|
fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 ||
|
||||||
|
!fileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("Geçersiz dosya adı.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputPath = Path.GetFullPath(ResolveWizardSeedOutputPath());
|
||||||
|
var filePath = Path.GetFullPath(Path.Combine(outputPath, fileName));
|
||||||
|
|
||||||
|
if (!filePath.StartsWith(outputPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new UserFriendlyException("Geçersiz dosya adı.");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new UserFriendlyException($"Dosya bulunamadı: {fileName}");
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task DeleteWizardDataAsync(WizardSeedFileDto seed)
|
private async Task DeleteWizardDataAsync(WizardSeedFileDto seed)
|
||||||
{
|
{
|
||||||
var ins = seed.InsertedRecords;
|
var ins = seed.InsertedRecords;
|
||||||
|
|
@ -767,35 +787,45 @@ public class ListFormWizardAppService(
|
||||||
if (commandType == SelectCommandTypeEnum.Query || commandType == SelectCommandTypeEnum.StoredProcedure)
|
if (commandType == SelectCommandTypeEnum.Query || commandType == SelectCommandTypeEnum.StoredProcedure)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(selectCommand))
|
||||||
|
return [];
|
||||||
|
|
||||||
var parts = selectCommand.Split('.');
|
var parts = selectCommand.Split('.');
|
||||||
var schemaName = parts.Length > 1 ? parts[0].Trim('"', '[', ']') : "dbo";
|
var schemaName = parts.Length > 1 ? parts[0].Trim('"', '[', ']') : "dbo";
|
||||||
var tableName = parts[parts.Length - 1].Trim('"', '[', ']');
|
var tableName = parts[^1].Trim('"', '[', ']');
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (repo, connectionString, dbType) = await _dynamicDataManager.GetAsync(false, dataSourceCode);
|
var (repo, connectionString, dbType) = await _dynamicDataManager.GetAsync(false, dataSourceCode);
|
||||||
|
|
||||||
var query = dbType == Sozsoft.Platform.Enums.DataSourceTypeEnum.Postgresql
|
// Şema/tablo adları kullanıcıdan geldiği için sorguya parametre olarak bağlanır.
|
||||||
? $"SELECT column_name FROM information_schema.columns WHERE table_schema = '{schemaName}' AND table_name = '{tableName}'"
|
var query = dbType == DataSourceTypeEnum.Postgresql
|
||||||
: $"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '{schemaName}' AND TABLE_NAME = '{tableName}'";
|
? "SELECT column_name FROM information_schema.columns WHERE table_schema = @schemaName AND table_name = @tableName"
|
||||||
|
: "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName";
|
||||||
|
|
||||||
var rows = await repo.QueryAsync(query, connectionString);
|
var parameters = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["schemaName"] = schemaName,
|
||||||
|
["tableName"] = tableName,
|
||||||
|
};
|
||||||
|
|
||||||
|
var rows = await repo.QueryAsync(query, connectionString, parameters);
|
||||||
|
|
||||||
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var row in rows)
|
foreach (var row in rows)
|
||||||
{
|
{
|
||||||
var dict = row as IDictionary<string, object>;
|
if (row is not IDictionary<string, object> dict)
|
||||||
if (dict != null)
|
continue;
|
||||||
{
|
|
||||||
var key = dict.ContainsKey("COLUMN_NAME") ? "COLUMN_NAME" : "column_name";
|
var key = dict.ContainsKey("COLUMN_NAME") ? "COLUMN_NAME" : "column_name";
|
||||||
if (dict.TryGetValue(key, out var col) && col != null)
|
if (dict.TryGetValue(key, out var col) && col != null)
|
||||||
columns.Add(col.ToString()!);
|
columns.Add(col.ToString()!);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
logger.LogWarning(ex, "Table columns could not be read for {DataSourceCode}/{SelectCommand}", dataSourceCode, selectCommand);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3620,13 +3620,13 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "Permission:Setup",
|
"key": "App.Setup",
|
||||||
"en": "Setup & Database Maintenance",
|
"en": "Setup & Database Maintenance",
|
||||||
"tr": "Kurulum ve Veritabanı Bakımı"
|
"tr": "Kurulum ve Veritabanı Bakımı"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "Permission:Setup.Migrate",
|
"key": "App.Setup.Migrate",
|
||||||
"en": "Run database migration",
|
"en": "Run database migration",
|
||||||
"tr": "Veritabanı geçişi çalıştırma"
|
"tr": "Veritabanı geçişi çalıştırma"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,22 @@
|
||||||
{
|
{
|
||||||
"Name": "App.Administration",
|
"Name": "App.Administration",
|
||||||
"DisplayName": "App.Administration"
|
"DisplayName": "App.Administration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "App.Setup",
|
||||||
|
"DisplayName": "App.Setup"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"PermissionDefinitionRecords": [
|
"PermissionDefinitionRecords": [
|
||||||
|
{
|
||||||
|
"GroupName": "App.Setup",
|
||||||
|
"Name": "App.Setup.Migrate",
|
||||||
|
"ParentName": null,
|
||||||
|
"DisplayName": "App.Setup.Migrate",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"MenuGroup": "Erp|Kurs"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "AbpTenantManagement.Tenants",
|
"Name": "AbpTenantManagement.Tenants",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { SelectBoxOption } from '@/types/shared'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Form, Formik, FormikProps } from 'formik'
|
import { Form, Formik, FormikProps } from 'formik'
|
||||||
import type { KeyboardEvent } from 'react'
|
import type { KeyboardEvent } from 'react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
@ -197,6 +197,79 @@ const step2ValidationSchema = Yup.object().shape({
|
||||||
|
|
||||||
const listFormValidationSchema = step1ValidationSchema.concat(step2ValidationSchema)
|
const listFormValidationSchema = step1ValidationSchema.concat(step2ValidationSchema)
|
||||||
|
|
||||||
|
const step1FieldNames = Object.keys(step1ValidationSchema.fields) as (keyof ListFormWizardDto)[]
|
||||||
|
const step2FieldNames = Object.keys(step2ValidationSchema.fields) as (keyof ListFormWizardDto)[]
|
||||||
|
|
||||||
|
// Audit columns that should not be selected by default
|
||||||
|
const AUDIT_COLUMNS = new Set([
|
||||||
|
'creationtime',
|
||||||
|
'creatorid',
|
||||||
|
'lastmodificationtime',
|
||||||
|
'lastmodifierid',
|
||||||
|
'isdeleted',
|
||||||
|
'deletiontime',
|
||||||
|
'deleterid',
|
||||||
|
])
|
||||||
|
|
||||||
|
const isAuditColumn = (columnName: string) => AUDIT_COLUMNS.has(columnName.toLowerCase())
|
||||||
|
const isTenantColumn = (columnName: string) => columnName.toLowerCase() === 'tenantid'
|
||||||
|
const isAutoSelectedColumn = (columnName: string, isTenant = false) =>
|
||||||
|
!isAuditColumn(columnName) && !(isTenant && isTenantColumn(columnName))
|
||||||
|
|
||||||
|
const removeTenantColumn = (columns: Set<string>) =>
|
||||||
|
new Set([...columns].filter((columnName) => !isTenantColumn(columnName)))
|
||||||
|
|
||||||
|
const removeTenantGroupItems = (groups: WizardGroup[]) =>
|
||||||
|
groups.map((group) => ({
|
||||||
|
...group,
|
||||||
|
items: group.items.filter((item) => !isTenantColumn(item.fieldName)),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Auto-derive listFormCode from wizardName
|
||||||
|
const deriveListFormCode = (name: string) => {
|
||||||
|
const sanitized = name.replace(/\s/g, '')
|
||||||
|
return sanitized ? `App.Wizard.${sanitized}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Yalnızca ilgili layout seçiliyken gösterilen adımlar. */
|
||||||
|
const OPTIONAL_STEPS = [
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
flag: 'todo',
|
||||||
|
translationKey: '::ListForms.ListFormEdit.TabTodo',
|
||||||
|
fallbackTitle: 'Todo / Kanban',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
flag: 'tree',
|
||||||
|
translationKey: '::ListForms.ListFormEdit.TabTree',
|
||||||
|
fallbackTitle: 'Tree',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
flag: 'gantt',
|
||||||
|
translationKey: '::ListForms.ListFormEdit.TabGantt',
|
||||||
|
fallbackTitle: 'Gantt',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
flag: 'scheduler',
|
||||||
|
translationKey: '::ListForms.ListFormEdit.TabScheduler',
|
||||||
|
fallbackTitle: 'Scheduler',
|
||||||
|
},
|
||||||
|
] as const satisfies ReadonlyArray<{
|
||||||
|
id: number
|
||||||
|
flag: keyof ListFormWizardDto
|
||||||
|
translationKey: string
|
||||||
|
fallbackTitle: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
const toSpacedLabel = (value: string) =>
|
||||||
|
value
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||||
|
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
|
||||||
|
.trim()
|
||||||
|
|
||||||
// ─── Wizard ───────────────────────────────────────────────────────────────────
|
// ─── Wizard ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const Wizard = () => {
|
const Wizard = () => {
|
||||||
|
|
@ -225,25 +298,32 @@ const Wizard = () => {
|
||||||
selectCommandType: SelectCommandTypeEnum
|
selectCommandType: SelectCommandTypeEnum
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const loadDbObjects = async (dsCode: string) => {
|
// Yalnızca son isteğin sonucu state'e yazılır (hızlı datasource değişiminde yarış koşulu).
|
||||||
|
const dbObjectsRequestRef = useRef(0)
|
||||||
|
|
||||||
|
const loadDbObjects = useCallback(async (dsCode: string) => {
|
||||||
|
const requestId = ++dbObjectsRequestRef.current
|
||||||
if (!dsCode) {
|
if (!dsCode) {
|
||||||
setDbObjects(null)
|
setDbObjects(null)
|
||||||
|
setIsLoadingDbObjects(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setIsLoadingDbObjects(true)
|
setIsLoadingDbObjects(true)
|
||||||
try {
|
try {
|
||||||
const res = await sqlObjectManagerService.getAllObjects(dsCode)
|
const res = await sqlObjectManagerService.getAllObjects(dsCode)
|
||||||
|
if (dbObjectsRequestRef.current !== requestId) return
|
||||||
setDbObjects(res.data)
|
setDbObjects(res.data)
|
||||||
} catch {
|
} catch {
|
||||||
|
if (dbObjectsRequestRef.current !== requestId) return
|
||||||
setDbObjects(null)
|
setDbObjects(null)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingDbObjects(false)
|
if (dbObjectsRequestRef.current === requestId) setIsLoadingDbObjects(false)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDbObjects(currentDataSource)
|
loadDbObjects(currentDataSource)
|
||||||
}, [currentDataSource])
|
}, [currentDataSource, loadDbObjects])
|
||||||
|
|
||||||
// When dbObjects become available in edit mode, load columns for the saved selectCommand
|
// When dbObjects become available in edit mode, load columns for the saved selectCommand
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -293,31 +373,6 @@ const Wizard = () => {
|
||||||
})
|
})
|
||||||
const [criteria, setCriteria] = useState<ListFormWorkflowCriteriaDto[]>([])
|
const [criteria, setCriteria] = useState<ListFormWorkflowCriteriaDto[]>([])
|
||||||
|
|
||||||
// Audit columns that should not be selected by default
|
|
||||||
const AUDIT_COLUMNS = new Set([
|
|
||||||
'creationtime',
|
|
||||||
'creatorid',
|
|
||||||
'lastmodificationtime',
|
|
||||||
'lastmodifierid',
|
|
||||||
'isdeleted',
|
|
||||||
'deletiontime',
|
|
||||||
'deleterid',
|
|
||||||
])
|
|
||||||
|
|
||||||
const isAuditColumn = (columnName: string) => AUDIT_COLUMNS.has(columnName.toLowerCase())
|
|
||||||
const isTenantColumn = (columnName: string) => columnName.toLowerCase() === 'tenantid'
|
|
||||||
const isAutoSelectedColumn = (columnName: string, isTenant = false) =>
|
|
||||||
!isAuditColumn(columnName) && !(isTenant && isTenantColumn(columnName))
|
|
||||||
|
|
||||||
const removeTenantColumn = (columns: Set<string>) =>
|
|
||||||
new Set([...columns].filter((columnName) => !isTenantColumn(columnName)))
|
|
||||||
|
|
||||||
const removeTenantGroupItems = (groups: WizardGroup[]) =>
|
|
||||||
groups.map((group) => ({
|
|
||||||
...group,
|
|
||||||
items: group.items.filter((item) => !isTenantColumn(item.fieldName)),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const loadColumns = async (dsCode: string, schema: string, name: string) => {
|
const loadColumns = async (dsCode: string, schema: string, name: string) => {
|
||||||
if (!dsCode || !name) {
|
if (!dsCode || !name) {
|
||||||
setSelectCommandColumns([])
|
setSelectCommandColumns([])
|
||||||
|
|
@ -368,7 +423,7 @@ const Wizard = () => {
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleAllColumns = (all: boolean, isTenant = formikRef.current?.values.isTenant ?? false) =>
|
const toggleAllColumns = (all: boolean, isTenant: boolean) =>
|
||||||
setSelectedColumns(
|
setSelectedColumns(
|
||||||
all
|
all
|
||||||
? new Set(
|
? new Set(
|
||||||
|
|
@ -469,14 +524,14 @@ const Wizard = () => {
|
||||||
confirmDelete: w.confirmDelete ?? true,
|
confirmDelete: w.confirmDelete ?? true,
|
||||||
allowDetail: w.allowDetail ?? false,
|
allowDetail: w.allowDetail ?? false,
|
||||||
defaultLayout: w.defaultLayout ?? 'grid',
|
defaultLayout: w.defaultLayout ?? 'grid',
|
||||||
grid: w.grid ?? true,
|
grid: w.grid ?? initialValues.grid,
|
||||||
card: w.card ?? true,
|
card: w.card ?? initialValues.card,
|
||||||
pivot: w.pivot ?? true,
|
pivot: w.pivot ?? initialValues.pivot,
|
||||||
tree: w.tree ?? true,
|
tree: w.tree ?? initialValues.tree,
|
||||||
chart: w.chart ?? true,
|
chart: w.chart ?? initialValues.chart,
|
||||||
gantt: w.gantt ?? true,
|
gantt: w.gantt ?? initialValues.gantt,
|
||||||
scheduler: w.scheduler ?? true,
|
scheduler: w.scheduler ?? initialValues.scheduler,
|
||||||
todo: w.todo ?? true,
|
todo: w.todo ?? initialValues.todo,
|
||||||
treeOptionDto: w.treeOptionDto ?? initialValues.treeOptionDto,
|
treeOptionDto: w.treeOptionDto ?? initialValues.treeOptionDto,
|
||||||
ganttOptionDto: w.ganttOptionDto ?? initialValues.ganttOptionDto,
|
ganttOptionDto: w.ganttOptionDto ?? initialValues.ganttOptionDto,
|
||||||
schedulerOptionDto: w.schedulerOptionDto ?? initialValues.schedulerOptionDto,
|
schedulerOptionDto: w.schedulerOptionDto ?? initialValues.schedulerOptionDto,
|
||||||
|
|
@ -578,18 +633,6 @@ const Wizard = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const formikRef = useRef<FormikProps<ListFormWizardDto>>(null)
|
const formikRef = useRef<FormikProps<ListFormWizardDto>>(null)
|
||||||
|
|
||||||
// Auto-derive listFormCode from wizardName
|
|
||||||
const deriveListFormCode = (name: string) => {
|
|
||||||
const sanitized = name.replace(/\s/g, '')
|
|
||||||
return sanitized ? `App.Wizard.${sanitized}` : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const toSpacedLabel = (value: string) =>
|
|
||||||
value
|
|
||||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
||||||
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
const handleWizardNameChange = (name: string) => {
|
const handleWizardNameChange = (name: string) => {
|
||||||
const formik = formikRef.current
|
const formik = formikRef.current
|
||||||
const spacedLabel = toSpacedLabel(name)
|
const spacedLabel = toSpacedLabel(name)
|
||||||
|
|
@ -710,24 +753,23 @@ const Wizard = () => {
|
||||||
await getMenuList()
|
await getMenuList()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNext = async () => {
|
/** Alanları touched yapar, hata yoksa hedef adıma geçer. */
|
||||||
if (!formikRef.current) return
|
const validateAndGo = async (fields: (keyof ListFormWizardDto)[], targetStep: number) => {
|
||||||
const errors = await formikRef.current.validateForm()
|
const formik = formikRef.current
|
||||||
const step1Fields = Object.keys(step1ValidationSchema.fields)
|
if (!formik) return
|
||||||
const hasStep1Errors = step1Fields.some((f) => errors[f as keyof ListFormWizardDto])
|
const errors = await formik.validateForm()
|
||||||
|
const touched = fields.reduce(
|
||||||
// Always mark step1 fields touched so validation errors become visible
|
|
||||||
const touchedStep1 = step1Fields.reduce(
|
|
||||||
(acc, key) => ({ ...acc, [key]: true }),
|
(acc, key) => ({ ...acc, [key]: true }),
|
||||||
{} as Record<string, boolean>,
|
{} as Record<string, boolean>,
|
||||||
)
|
)
|
||||||
await formikRef.current.setTouched({ ...formikRef.current.touched, ...touchedStep1 })
|
await formik.setTouched({ ...formik.touched, ...touched })
|
||||||
|
if (fields.some((field) => errors[field])) return
|
||||||
// Also require wizardName
|
setCurrentStep(targetStep)
|
||||||
if (hasStep1Errors) return
|
|
||||||
setCurrentStep(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleNext = () => validateAndGo(step1FieldNames, 1)
|
||||||
|
const handleNext2 = () => validateAndGo(step2FieldNames, 2)
|
||||||
|
|
||||||
const handleBack = () => setCurrentStep(0)
|
const handleBack = () => setCurrentStep(0)
|
||||||
const { getConfig } = useStoreActions((a) => a.abpConfig)
|
const { getConfig } = useStoreActions((a) => a.abpConfig)
|
||||||
|
|
||||||
|
|
@ -744,32 +786,18 @@ const Wizard = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNext2 = async () => {
|
|
||||||
if (!formikRef.current) return
|
|
||||||
const errors = await formikRef.current.validateForm()
|
|
||||||
const step2Fields = Object.keys(step2ValidationSchema.fields)
|
|
||||||
const hasStep2Errors = step2Fields.some((f) => errors[f as keyof ListFormWizardDto])
|
|
||||||
const touchedStep2 = step2Fields.reduce(
|
|
||||||
(acc, key) => ({ ...acc, [key]: true }),
|
|
||||||
{} as Record<string, boolean>,
|
|
||||||
)
|
|
||||||
await formikRef.current.setTouched({ ...formikRef.current.touched, ...touchedStep2 })
|
|
||||||
if (hasStep2Errors) return
|
|
||||||
setCurrentStep(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeploy = async () => {
|
const handleDeploy = async () => {
|
||||||
|
const formik = formikRef.current
|
||||||
|
if (!formik) throw new Error(translate('::App.Platform.Unknown') || 'Form bulunamadı')
|
||||||
|
|
||||||
|
const values = formik.values
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!formikRef.current) throw new Error('Form bulunamadı')
|
|
||||||
|
|
||||||
const values = formikRef.current.values
|
|
||||||
|
|
||||||
// Edit modunda: önce eski dosyayı sil (DB kayıtlarını temizler)
|
// Edit modunda: önce eski dosyayı sil (DB kayıtlarını temizler)
|
||||||
if (isEditMode && editFileName) {
|
if (isEditMode && editFileName) {
|
||||||
await deleteWizardFile(editFileName)
|
await deleteWizardFile(editFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔴 Önce kayıt işlemi TAMAMLANSIN
|
|
||||||
await postListFormWizard({
|
await postListFormWizard({
|
||||||
...values,
|
...values,
|
||||||
groups: editingGroups.map((g) => ({
|
groups: editingGroups.map((g) => ({
|
||||||
|
|
@ -786,7 +814,7 @@ const Wizard = () => {
|
||||||
colSpan: item.colSpan,
|
colSpan: item.colSpan,
|
||||||
isRequired: item.isRequired,
|
isRequired: item.isRequired,
|
||||||
includeInEditingForm: item.includeInEditingForm,
|
includeInEditingForm: item.includeInEditingForm,
|
||||||
dbSourceType: col ? sqlDataTypeToDbType(col.dataType) : 12,
|
dbSourceType: col ? sqlDataTypeToDbType(col.dataType) : DbTypeEnum.String,
|
||||||
turkishCaption: item.turkishCaption,
|
turkishCaption: item.turkishCaption,
|
||||||
englishCaption: item.englishCaption,
|
englishCaption: item.englishCaption,
|
||||||
captionName: item.captionName,
|
captionName: item.captionName,
|
||||||
|
|
@ -805,23 +833,28 @@ const Wizard = () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✅ sonra config çek
|
// Menü/permission değişikliklerinin görünmesi için ABP config'i tazele
|
||||||
await getConfig(true)
|
await getConfig(true)
|
||||||
|
|
||||||
// ✅ sonra navigate
|
|
||||||
navigate(ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode), {
|
|
||||||
replace: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// ✅ en son kullanıcıya mesaj
|
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="success" duration={2000}>
|
<Notification type="success" duration={2000}>
|
||||||
{translate('::ListForms.FormBilgileriKaydedildi')}
|
{translate('::ListForms.FormBilgileriKaydedildi')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
navigate(ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode), {
|
||||||
|
replace: true,
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.push(
|
||||||
|
<Notification type="danger">
|
||||||
|
{err?.message ?? translate('::App.Platform.Unknown') ?? 'Unknown error'}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'bottom-end' },
|
||||||
|
)
|
||||||
|
// Deploy adımının hatayı gösterebilmesi için hatayı yeniden fırlat
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -870,43 +903,21 @@ const Wizard = () => {
|
||||||
id: 5,
|
id: 5,
|
||||||
title: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow',
|
title: translate('::ListForms.ListFormEdit.TabWorkflow') || 'Workflow',
|
||||||
},
|
},
|
||||||
...(values.todo
|
...OPTIONAL_STEPS.filter((step) => values[step.flag]).map((step) => ({
|
||||||
? [
|
id: step.id,
|
||||||
{
|
title: translate(step.translationKey) || step.fallbackTitle,
|
||||||
id: 6,
|
})),
|
||||||
title: translate('::ListForms.ListFormEdit.TabTodo') || 'Todo / Kanban',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(values.tree
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
title: translate('::ListForms.ListFormEdit.TabTree') || 'Tree',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(values.gantt
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
title: translate('::ListForms.ListFormEdit.TabGantt') || 'Gantt',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(values.scheduler
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
title: translate('::ListForms.ListFormEdit.TabScheduler') || 'Scheduler',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{ id: 10, title: translate('::App.Platform.Deploy') || 'Deploy' },
|
{ id: 10, title: translate('::App.Platform.Deploy') || 'Deploy' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Opsiyonel bir adım (todo/tree/gantt/scheduler) sonradan kapatılırsa
|
||||||
|
// currentStep listede kalmayabilir; boş ekran yerine en yakın geçerli adıma düş.
|
||||||
|
const resolvedStep = activeSteps.some((step) => step.id === currentStep)
|
||||||
|
? currentStep
|
||||||
|
: (activeSteps.filter((step) => step.id < currentStep).pop()?.id ?? 0)
|
||||||
|
|
||||||
const goToAdjacentStep = (direction: -1 | 1) => {
|
const goToAdjacentStep = (direction: -1 | 1) => {
|
||||||
const currentIndex = activeSteps.findIndex((step) => step.id === currentStep)
|
const currentIndex = activeSteps.findIndex((step) => step.id === resolvedStep)
|
||||||
const targetStep = activeSteps[currentIndex + direction]
|
const targetStep = activeSteps[currentIndex + direction]
|
||||||
if (targetStep) {
|
if (targetStep) {
|
||||||
setCurrentStep(targetStep.id)
|
setCurrentStep(targetStep.id)
|
||||||
|
|
@ -917,7 +928,7 @@ const Wizard = () => {
|
||||||
<>
|
<>
|
||||||
<div className="mb-6 mt-2">
|
<div className="mb-6 mt-2">
|
||||||
<Steps
|
<Steps
|
||||||
current={activeSteps.findIndex((step) => step.id === currentStep) + 1}
|
current={activeSteps.findIndex((step) => step.id === resolvedStep) + 1}
|
||||||
className="flex flex-row flex-wrap !justify-start gap-y-2 lg:flex-nowrap lg:!justify-between"
|
className="flex flex-row flex-wrap !justify-start gap-y-2 lg:flex-nowrap lg:!justify-between"
|
||||||
>
|
>
|
||||||
<Steps.Item
|
<Steps.Item
|
||||||
|
|
@ -935,9 +946,9 @@ const Wizard = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form onKeyDown={preventEnterSubmit}>
|
<Form onKeyDown={preventEnterSubmit}>
|
||||||
<FormContainer size={currentStep >= 2 ? undefined : 'sm'}>
|
<FormContainer size={resolvedStep >= 2 ? undefined : 'sm'}>
|
||||||
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
||||||
{currentStep === 0 && (
|
{resolvedStep === 0 && (
|
||||||
<WizardStep1
|
<WizardStep1
|
||||||
values={values}
|
values={values}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
|
@ -963,7 +974,7 @@ const Wizard = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 2: Data Settings ───────────────────────────── */}
|
{/* ─── Step 2: Data Settings ───────────────────────────── */}
|
||||||
{currentStep === 1 && (
|
{resolvedStep === 1 && (
|
||||||
<WizardStep2
|
<WizardStep2
|
||||||
values={values}
|
values={values}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
|
@ -994,7 +1005,7 @@ const Wizard = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 3: List Form Fields ───────────────────────────── */}
|
{/* ─── Step 3: List Form Fields ───────────────────────────── */}
|
||||||
{currentStep === 2 && (
|
{resolvedStep === 2 && (
|
||||||
<WizardStep3
|
<WizardStep3
|
||||||
selectedColumns={selectedColumns}
|
selectedColumns={selectedColumns}
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
|
|
@ -1009,7 +1020,7 @@ const Wizard = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 4: Sub Forms ───────────────────────────── */}
|
{/* ─── Step 4: Sub Forms ───────────────────────────── */}
|
||||||
{currentStep === 3 && (
|
{resolvedStep === 3 && (
|
||||||
<WizardStep4
|
<WizardStep4
|
||||||
subForms={subForms}
|
subForms={subForms}
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
|
|
@ -1021,7 +1032,7 @@ const Wizard = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 5: Widgets ───────────────────────────── */}
|
{/* ─── Step 5: Widgets ───────────────────────────── */}
|
||||||
{currentStep === 4 && (
|
{resolvedStep === 4 && (
|
||||||
<WizardStep5
|
<WizardStep5
|
||||||
widgets={widgets}
|
widgets={widgets}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
|
|
@ -1032,7 +1043,7 @@ const Wizard = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 6: Workflow ───────────────────────────── */}
|
{/* ─── Step 6: Workflow ───────────────────────────── */}
|
||||||
{currentStep === 5 && (
|
{resolvedStep === 5 && (
|
||||||
<WizardStep6
|
<WizardStep6
|
||||||
listFormCode={values.listFormCode}
|
listFormCode={values.listFormCode}
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
|
|
@ -1047,7 +1058,7 @@ const Wizard = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 7: Todo / Kanban ────────────────────── */}
|
{/* ─── Step 7: Todo / Kanban ────────────────────── */}
|
||||||
{currentStep === 6 && values.todo && (
|
{resolvedStep === 6 && values.todo && (
|
||||||
<WizardStep7
|
<WizardStep7
|
||||||
selectedColumns={selectedColumns}
|
selectedColumns={selectedColumns}
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
|
|
@ -1058,7 +1069,7 @@ const Wizard = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 8: Tree ─────────────────────────────── */}
|
{/* ─── Step 8: Tree ─────────────────────────────── */}
|
||||||
{currentStep === 7 && values.tree && (
|
{resolvedStep === 7 && values.tree && (
|
||||||
<WizardStep8
|
<WizardStep8
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
|
|
@ -1068,7 +1079,7 @@ const Wizard = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 9: Gantt ────────────────────────────── */}
|
{/* ─── Step 9: Gantt ────────────────────────────── */}
|
||||||
{currentStep === 8 && values.gantt && (
|
{resolvedStep === 8 && values.gantt && (
|
||||||
<WizardStep9
|
<WizardStep9
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
|
|
@ -1078,7 +1089,7 @@ const Wizard = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 10: Scheduler ───────────────────────── */}
|
{/* ─── Step 10: Scheduler ───────────────────────── */}
|
||||||
{currentStep === 9 && values.scheduler && (
|
{resolvedStep === 9 && values.scheduler && (
|
||||||
<WizardStep10
|
<WizardStep10
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
|
|
@ -1088,7 +1099,7 @@ const Wizard = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 11: Deploy ──────────────────────────── */}
|
{/* ─── Step 11: Deploy ──────────────────────────── */}
|
||||||
{currentStep === 10 && (
|
{resolvedStep === 10 && (
|
||||||
<WizardStep11
|
<WizardStep11
|
||||||
values={values}
|
values={values}
|
||||||
wizardName={values.wizardName}
|
wizardName={values.wizardName}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { Badge, Button, Input, Notification, toast } from '@/components/ui'
|
import { Badge, Button, Dialog, Input, Notification, toast } from '@/components/ui'
|
||||||
import Container from '@/components/shared/Container'
|
import Container from '@/components/shared/Container'
|
||||||
import {
|
import {
|
||||||
FaTrash,
|
FaTrash,
|
||||||
|
|
@ -38,9 +38,6 @@ const WizardFileManager = () => {
|
||||||
const [deletingFile, setDeletingFile] = useState<string | null>(null)
|
const [deletingFile, setDeletingFile] = useState<string | null>(null)
|
||||||
const [confirm, setConfirm] = useState<ConfirmState | null>(null)
|
const [confirm, setConfirm] = useState<ConfirmState | null>(null)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const IconComponent = (icon: string) => {
|
|
||||||
return navigationIcon[icon] || FaDatabase // default icon
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredFiles = useMemo(() => {
|
const filteredFiles = useMemo(() => {
|
||||||
const q = search.trim().toLowerCase()
|
const q = search.trim().toLowerCase()
|
||||||
|
|
@ -67,7 +64,7 @@ const WizardFileManager = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [translate])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFiles()
|
loadFiles()
|
||||||
|
|
@ -168,7 +165,7 @@ const WizardFileManager = () => {
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{filteredFiles.map((f) => {
|
{filteredFiles.map((f) => {
|
||||||
const Icon = IconComponent(f.menuIcon)
|
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={f.fileName}
|
key={f.fileName}
|
||||||
|
|
@ -191,7 +188,10 @@ const WizardFileManager = () => {
|
||||||
<div className="flex items-center gap-2 shrink-0 sm:ml-3">
|
<div className="flex items-center gap-2 shrink-0 sm:ml-3">
|
||||||
{!f.hasInsertedRecords && (
|
{!f.hasInsertedRecords && (
|
||||||
<span
|
<span
|
||||||
title="Bu dosyada izlenen kayıt bilgisi yok. Eski format olabilir."
|
title={
|
||||||
|
translate('::App.Listforms.WizardFileNoTrackedRecords') ||
|
||||||
|
'Bu dosyada izlenen kayıt bilgisi yok. Eski format olabilir.'
|
||||||
|
}
|
||||||
className="text-yellow-500 text-xs flex items-center gap-1"
|
className="text-yellow-500 text-xs flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<FaExclamationTriangle />
|
<FaExclamationTriangle />
|
||||||
|
|
@ -232,36 +232,34 @@ const WizardFileManager = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Delete Confirm Dialog */}
|
{/* Delete Confirm Dialog */}
|
||||||
{confirm && (
|
<Dialog
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
isOpen={confirm !== null}
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-xl p-6 max-w-sm w-full mx-4">
|
onClose={() => setConfirm(null)}
|
||||||
<div className="flex items-center gap-3 mb-4">
|
onRequestClose={() => setConfirm(null)}
|
||||||
<FaExclamationTriangle className="text-red-500 text-xl shrink-0" />
|
>
|
||||||
|
<Dialog.Body>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<FaExclamationTriangle className="text-red-500 text-xl shrink-0 mt-1" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-gray-800 dark:text-gray-200">
|
<p className="font-semibold text-gray-800 dark:text-gray-200">
|
||||||
{translate('::App.Platform.DeleteAction')}
|
{translate('::App.Platform.DeleteAction')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
<strong>{confirm?.wizardName}</strong>{' '}
|
||||||
{translate('::App.Listforms.WizardFileDeleteConfirm')}
|
{translate('::App.Listforms.WizardFileDeleteConfirm')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 mt-4">
|
</Dialog.Body>
|
||||||
|
<Dialog.Footer className="flex justify-end gap-2">
|
||||||
<Button size="sm" variant="plain" type="button" onClick={() => setConfirm(null)}>
|
<Button size="sm" variant="plain" type="button" onClick={() => setConfirm(null)}>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button size="sm" variant="solid" type="button" onClick={handleDeleteConfirm}>
|
||||||
size="sm"
|
|
||||||
variant="solid"
|
|
||||||
type="button"
|
|
||||||
onClick={handleDeleteConfirm}
|
|
||||||
>
|
|
||||||
{translate('::Delete') || 'Yes, Delete'}
|
{translate('::Delete') || 'Yes, Delete'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Dialog.Footer>
|
||||||
</div>
|
</Dialog>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,12 +66,25 @@ export function filterNonLinkNodes(nodes: MenuTreeNode[]): MenuTreeNode[] {
|
||||||
.map((n) => ({ ...n, children: filterNonLinkNodes(n.children) }))
|
.map((n) => ({ ...n, children: filterNonLinkNodes(n.children) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** code → parentCode haritası; her aramada listeyi taramamak için. */
|
||||||
|
function buildParentMap(rawItems: MenuItem[]): Map<string, string | undefined> {
|
||||||
|
const map = new Map<string, string | undefined>()
|
||||||
|
rawItems.forEach((item) => {
|
||||||
|
if (item.code) map.set(item.code, item.parentCode ?? undefined)
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
export function findRootCode(rawItems: MenuItem[], code: string): string {
|
export function findRootCode(rawItems: MenuItem[], code: string): string {
|
||||||
|
const parents = buildParentMap(rawItems)
|
||||||
|
const visited = new Set<string>()
|
||||||
let current = code
|
let current = code
|
||||||
for (let i = 0; i < 50; i++) {
|
// visited: veri bozulsa bile (döngüsel parent) sonsuz döngüye girilmez
|
||||||
const item = rawItems.find((r) => r.code === current)
|
while (!visited.has(current)) {
|
||||||
if (!item?.parentCode) return current
|
visited.add(current)
|
||||||
current = item.parentCode
|
const parentCode = parents.get(current)
|
||||||
|
if (!parentCode) return current
|
||||||
|
current = parentCode
|
||||||
}
|
}
|
||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
|
|
@ -270,13 +283,14 @@ function TreeNode({
|
||||||
|
|
||||||
/** Returns all ancestor codes of `code` in the flat rawItems list */
|
/** Returns all ancestor codes of `code` in the flat rawItems list */
|
||||||
function getAncestorCodes(rawItems: MenuItem[], code: string): Set<string> {
|
function getAncestorCodes(rawItems: MenuItem[], code: string): Set<string> {
|
||||||
|
const parents = buildParentMap(rawItems)
|
||||||
const result = new Set<string>()
|
const result = new Set<string>()
|
||||||
let current = code
|
let current = code
|
||||||
for (let i = 0; i < 50; i++) {
|
while (true) {
|
||||||
const item = rawItems.find((r) => r.code === current)
|
const parentCode = parents.get(current)
|
||||||
if (!item?.parentCode) break
|
if (!parentCode || result.has(parentCode)) break
|
||||||
result.add(item.parentCode)
|
result.add(parentCode)
|
||||||
current = item.parentCode
|
current = parentCode
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
@ -375,12 +389,16 @@ function MenuTreeInline({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function enrichNode(node: MenuTreeNode): MenuTreeNode & { id?: string } {
|
// Menü ağacı her render'da yeniden dolaşılmasın; id eşlemesi tek haritadan yapılır.
|
||||||
const raw = rawItems.find((i) => i.code === node.code)
|
const enrichedNodes = useMemo(() => {
|
||||||
return { ...node, id: raw?.id, children: node.children.map(enrichNode) }
|
const idByCode = new Map(rawItems.map((item) => [item.code, item.id]))
|
||||||
}
|
const enrichNode = (node: MenuTreeNode): MenuTreeNode & { id?: string } => ({
|
||||||
|
...node,
|
||||||
const enrichedNodes = nodes.map(enrichNode)
|
id: idByCode.get(node.code),
|
||||||
|
children: node.children.map(enrichNode),
|
||||||
|
})
|
||||||
|
return nodes.map(enrichNode)
|
||||||
|
}, [nodes, rawItems])
|
||||||
|
|
||||||
const sharedNodeProps = {
|
const sharedNodeProps = {
|
||||||
expanded,
|
expanded,
|
||||||
|
|
@ -483,6 +501,11 @@ const WizardStep1 = ({
|
||||||
|
|
||||||
const step1Missing = [
|
const step1Missing = [
|
||||||
!wizardName && translate('::ListForms.Wizard.Step1.WizardName'),
|
!wizardName && translate('::ListForms.Wizard.Step1.WizardName'),
|
||||||
|
!values.menuCode && translate('::ListForms.Wizard.Step1.MenuCode'),
|
||||||
|
// Yup şeması menuOrder >= 1 istiyor; başlangıç değeri 0 olduğu için
|
||||||
|
// burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu.
|
||||||
|
!(Number(values.menuOrder) >= 1) &&
|
||||||
|
(translate('::ListForms.Wizard.Step1.MenuOrder') || 'Menu Order'),
|
||||||
!values.permissionGroupName && translate('::ListForms.Wizard.Step1.PermissionGroupName'),
|
!values.permissionGroupName && translate('::ListForms.Wizard.Step1.PermissionGroupName'),
|
||||||
!values.languageTextMenuEn && translate('::ListForms.Wizard.Step4.MenuEn'),
|
!values.languageTextMenuEn && translate('::ListForms.Wizard.Step4.MenuEn'),
|
||||||
!values.languageTextMenuTr && translate('::ListForms.Wizard.Step4.MenuTr'),
|
!values.languageTextMenuTr && translate('::ListForms.Wizard.Step4.MenuTr'),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button } from '@/components/ui'
|
import { Button } from '@/components/ui'
|
||||||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||||
import React, { useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
FaArrowLeft,
|
FaArrowLeft,
|
||||||
FaCheckCircle,
|
FaCheckCircle,
|
||||||
|
|
@ -34,7 +34,8 @@ export interface WizardStep11Props {
|
||||||
widgets: WidgetEditDto[]
|
widgets: WidgetEditDto[]
|
||||||
workflow: WorkflowDto
|
workflow: WorkflowDto
|
||||||
criteria: ListFormWorkflowCriteriaDto[]
|
criteria: ListFormWorkflowCriteriaDto[]
|
||||||
translate: (key: string) => string
|
// Log satırları {0} gibi parametreler içerdiği için params destekli imza gerekli
|
||||||
|
translate: (key: string, params?: Record<string, string | number>) => string
|
||||||
onBack: () => void
|
onBack: () => void
|
||||||
onSubmit: () => Promise<void>
|
onSubmit: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +49,18 @@ interface LogEntry {
|
||||||
detail?: string
|
detail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bağlantı cümlesi özet ekranında açıkça gösterilmez; yalnızca kullanıcının
|
||||||
|
* doğru kaynağı seçtiğini anlamasına yetecek kadarı bırakılıp gerisi maskelenir.
|
||||||
|
*/
|
||||||
|
function maskConnectionString(value?: string) {
|
||||||
|
if (!value) return value
|
||||||
|
return value.replace(
|
||||||
|
/(password|pwd|user id|uid|username|account key|accountkey)\s*=\s*[^;]*/gi,
|
||||||
|
(_match, key: string) => `${key}=••••••`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Deploy log steps ─────────────────────────────────────────────────────────
|
// ─── Deploy log steps ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function buildLogSteps(
|
function buildLogSteps(
|
||||||
|
|
@ -175,7 +188,10 @@ const WizardStep11 = ({
|
||||||
const [isDone, setIsDone] = useState(false)
|
const [isDone, setIsDone] = useState(false)
|
||||||
const [hasError, setHasError] = useState(false)
|
const [hasError, setHasError] = useState(false)
|
||||||
|
|
||||||
const steps = buildLogSteps(values, groups, translate)
|
const steps = useMemo(
|
||||||
|
() => buildLogSteps(values, groups, translate),
|
||||||
|
[values, groups, translate],
|
||||||
|
)
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||||
|
|
||||||
|
|
@ -308,7 +324,7 @@ const WizardStep11 = ({
|
||||||
/>
|
/>
|
||||||
<Row
|
<Row
|
||||||
label={translate('::App.Listform.ListformField.ConnectionString')}
|
label={translate('::App.Listform.ListformField.ConnectionString')}
|
||||||
value={values.dataSourceConnectionString}
|
value={maskConnectionString(values.dataSourceConnectionString)}
|
||||||
/>
|
/>
|
||||||
<Row
|
<Row
|
||||||
label={translate('::ListForms.Wizard.Step4.SelectCommand')}
|
label={translate('::ListForms.Wizard.Step4.SelectCommand')}
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,10 @@ const WizardStep2 = ({
|
||||||
const step2Missing = [
|
const step2Missing = [
|
||||||
!values.listFormCode && translate('::App.Listform.ListformField.ListFormCode'),
|
!values.listFormCode && translate('::App.Listform.ListformField.ListFormCode'),
|
||||||
!values.dataSourceCode && translate('::ListForms.Wizard.Step4.DataSource'),
|
!values.dataSourceCode && translate('::ListForms.Wizard.Step4.DataSource'),
|
||||||
|
// Yeni veri kaynağı seçildiyse bağlantı cümlesi zorunlu (backend de doğruluyor)
|
||||||
|
isDataSourceNew &&
|
||||||
|
!values.dataSourceConnectionString?.trim() &&
|
||||||
|
translate('::App.Listform.ListformField.ConnectionString'),
|
||||||
!values.selectCommand && translate('::ListForms.Wizard.Step2.SelectCommand'),
|
!values.selectCommand && translate('::ListForms.Wizard.Step2.SelectCommand'),
|
||||||
!values.keyFieldName && translate('::ListForms.Wizard.Step4.KeyField'),
|
!values.keyFieldName && translate('::ListForms.Wizard.Step4.KeyField'),
|
||||||
!values.languageTextDescEn && translate('::ListForms.Wizard.Step2.DescriptionTextEnglish'),
|
!values.languageTextDescEn && translate('::ListForms.Wizard.Step2.DescriptionTextEnglish'),
|
||||||
|
|
@ -188,12 +192,15 @@ const WizardStep2 = ({
|
||||||
{isDataSourceNew && (
|
{isDataSourceNew && (
|
||||||
<FormItem
|
<FormItem
|
||||||
label={translate('::App.Listform.ListformField.ConnectionString')}
|
label={translate('::App.Listform.ListformField.ConnectionString')}
|
||||||
|
asterisk={true}
|
||||||
invalid={!!(errors.dataSourceConnectionString && touched.dataSourceConnectionString)}
|
invalid={!!(errors.dataSourceConnectionString && touched.dataSourceConnectionString)}
|
||||||
errorMessage={errors.dataSourceConnectionString}
|
errorMessage={errors.dataSourceConnectionString}
|
||||||
>
|
>
|
||||||
|
{/* Bağlantı cümlesi kimlik bilgisi içerebilir; ekranda ve tarayıcı
|
||||||
|
otomatik doldurmasında açıkta bırakılmaz. */}
|
||||||
<Field
|
<Field
|
||||||
type="text"
|
type="password"
|
||||||
autoComplete="off"
|
autoComplete="new-password"
|
||||||
name="dataSourceConnectionString"
|
name="dataSourceConnectionString"
|
||||||
placeholder={translate('::App.Listform.ListformField.ConnectionString')}
|
placeholder={translate('::App.Listform.ListformField.ConnectionString')}
|
||||||
component={Input}
|
component={Input}
|
||||||
|
|
|
||||||
|
|
@ -109,10 +109,18 @@ const formatLabel = (text: string) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DnD ve React key'leri için benzersiz kimlik.
|
||||||
|
* `Date.now()` tek başına yeterli değil: "Tümünü Ekle" aynı milisaniyede
|
||||||
|
* birden fazla item üretip çakışan id'lere yol açıyordu.
|
||||||
|
*/
|
||||||
|
let idCounter = 0
|
||||||
|
const nextId = (prefix: string) => `${prefix}_${Date.now()}_${++idCounter}`
|
||||||
|
|
||||||
function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupItem {
|
function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupItem {
|
||||||
const sqlType = meta?.dataType ?? ''
|
const sqlType = meta?.dataType ?? ''
|
||||||
return {
|
return {
|
||||||
id: `${colName}_${Date.now()}`,
|
id: nextId(colName),
|
||||||
fieldName: colName,
|
fieldName: colName,
|
||||||
editorType: inferEditorType(sqlType),
|
editorType: inferEditorType(sqlType),
|
||||||
editorOptions: '',
|
editorOptions: '',
|
||||||
|
|
@ -132,7 +140,7 @@ function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupIte
|
||||||
|
|
||||||
function newGroup(order: number): WizardGroup {
|
function newGroup(order: number): WizardGroup {
|
||||||
return {
|
return {
|
||||||
id: `grp_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
id: nextId('grp'),
|
||||||
caption: `Group ${order}`,
|
caption: `Group ${order}`,
|
||||||
colCount: 2,
|
colCount: 2,
|
||||||
items: [],
|
items: [],
|
||||||
|
|
@ -864,22 +872,13 @@ const WizardStep3 = ({
|
||||||
// 2. Ensure at least one empty group exists for the user to drag into
|
// 2. Ensure at least one empty group exists for the user to drag into
|
||||||
const finalGroups =
|
const finalGroups =
|
||||||
cleaned.length === 0
|
cleaned.length === 0
|
||||||
? [
|
? [{ id: nextId('grp_default'), caption: '', colCount: 2, items: [] as WizardGroupItem[] }]
|
||||||
{
|
|
||||||
id: `grp_default_${Date.now()}`,
|
|
||||||
caption: '',
|
|
||||||
colCount: 2,
|
|
||||||
items: [] as WizardGroupItem[],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: cleaned
|
: cleaned
|
||||||
|
|
||||||
// 3. Only update if something actually changed
|
// 3. Yalnızca gerçekten bir item düştüyse (veya varsayılan grup eklendiyse) güncelle.
|
||||||
const itemsChanged = finalGroups.some(
|
// Uzunluk karşılaştırması aynı sayıda ekleme/çıkarma durumunu kaçırıyordu.
|
||||||
(g, i) => g.items.length !== (groups[i]?.items.length ?? -1),
|
const removedItem = cleaned.some((g, i) => g.items.length !== groups[i].items.length)
|
||||||
)
|
if (removedItem || finalGroups.length !== groups.length) onGroupsChange(finalGroups)
|
||||||
const countChanged = finalGroups.length !== groups.length
|
|
||||||
if (itemsChanged || countChanged) onGroupsChange(finalGroups)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedColumns])
|
}, [selectedColumns])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,9 @@ const schema = object().shape({
|
||||||
code: string().required(),
|
code: string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const emptySubForm: SubFormDto = {
|
// Her yeni kayıt için ayrı nesne üretilir; aksi halde eklenen tüm sub-form'lar
|
||||||
|
// aynı relation dizisini / URLSearchParams örneğini paylaşırdı.
|
||||||
|
const createEmptySubForm = (): SubFormDto => ({
|
||||||
tabTitle: '',
|
tabTitle: '',
|
||||||
tabType: SubFormTabTypeEnum.List,
|
tabType: SubFormTabTypeEnum.List,
|
||||||
code: '',
|
code: '',
|
||||||
|
|
@ -47,7 +49,7 @@ const emptySubForm: SubFormDto = {
|
||||||
relation: [],
|
relation: [],
|
||||||
tabMode: 'edit',
|
tabMode: 'edit',
|
||||||
searchParams: new URLSearchParams(),
|
searchParams: new URLSearchParams(),
|
||||||
}
|
})
|
||||||
|
|
||||||
function WizardStep4({
|
function WizardStep4({
|
||||||
subForms,
|
subForms,
|
||||||
|
|
@ -243,7 +245,7 @@ function WizardStep4({
|
||||||
>
|
>
|
||||||
{dialogIndex !== null && (
|
{dialogIndex !== null && (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={dialogIndex === -1 ? emptySubForm : subForms[dialogIndex]}
|
initialValues={dialogIndex === -1 ? createEmptySubForm() : subForms[dialogIndex]}
|
||||||
validationSchema={schema}
|
validationSchema={schema}
|
||||||
onSubmit={upsertSubForm}
|
onSubmit={upsertSubForm}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,10 @@ function WizardStep6({
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUsers(0, 1000).then((response) => {
|
let cancelled = false
|
||||||
|
getUsers(0, 1000)
|
||||||
|
.then((response) => {
|
||||||
|
if (cancelled) return
|
||||||
setUserList(
|
setUserList(
|
||||||
(response.data?.items ?? []).map((user: IdentityUserDto) => ({
|
(response.data?.items ?? []).map((user: IdentityUserDto) => ({
|
||||||
value: user.userName,
|
value: user.userName,
|
||||||
|
|
@ -89,6 +92,12 @@ function WizardStep6({
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setUserList([])
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,8 @@ function FlowNode({
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="none"
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute z-40 grid !h-32 !w-44 touch-none content-start justify-items-start gap-1 rounded-lg border-2 border-[#667085] !bg-white !p-2.5 text-left text-slate-700 shadow-lg hover:!bg-white active:!bg-white dark:border-gray-600 dark:!bg-gray-900 dark:text-gray-200 dark:hover:!bg-gray-900 dark:active:!bg-gray-900',
|
// Metin renkleri `!` ile: Button variant'ının kendi text rengini ezmesi gerekiyor.
|
||||||
|
'absolute z-40 grid !h-32 !w-44 touch-none content-start justify-items-start gap-1 rounded-lg border-2 border-[#667085] !bg-white !p-2.5 text-left !text-slate-700 shadow-lg hover:!bg-white active:!bg-white dark:border-gray-600 dark:!bg-gray-900 dark:!text-gray-200 dark:hover:!bg-gray-900 dark:active:!bg-gray-900',
|
||||||
{
|
{
|
||||||
'border-blue-600 outline outline-[3px] outline-blue-600/20': selected,
|
'border-blue-600 outline outline-[3px] outline-blue-600/20': selected,
|
||||||
'border-green-600 !bg-green-50 shadow-[0_0_0_4px_rgba(22,163,74,0.18),0_10px_24px_rgba(22,101,52,0.14)] hover:!bg-green-50 active:!bg-green-50 dark:!bg-green-900/20 dark:hover:!bg-green-900/20 dark:active:!bg-green-900/20':
|
'border-green-600 !bg-green-50 shadow-[0_0_0_4px_rgba(22,163,74,0.18),0_10px_24px_rgba(22,101,52,0.14)] hover:!bg-green-50 active:!bg-green-50 dark:!bg-green-900/20 dark:hover:!bg-green-900/20 dark:active:!bg-green-900/20':
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,24 @@ const designerIconButtonClass =
|
||||||
|
|
||||||
const designerTabClass = '!h-auto min-h-8 rounded-md border !px-3 py-1.5 transition-colors'
|
const designerTabClass = '!h-auto min-h-8 rounded-md border !px-3 py-1.5 transition-colors'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button bileşeni variant'a göre kendi metin rengini basıyor
|
||||||
|
* (`solid` → `text-white`, `plain` → `text-gray-600`). Aynı özgüllükteki
|
||||||
|
* yardımcı sınıflar CSS sırasına göre eziliyor ve beyaz zeminde beyaz yazı
|
||||||
|
* oluşuyordu. Bu yüzden metin renkleri `!` ile önceliklendirilir.
|
||||||
|
*/
|
||||||
|
const designerNeutralColorClass =
|
||||||
|
'border-gray-300 !bg-white !text-slate-700 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:!text-gray-200 dark:hover:!bg-gray-700'
|
||||||
|
|
||||||
|
const designerAccentColorClass =
|
||||||
|
'border-blue-600 !bg-white !text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:!text-blue-300 dark:hover:!bg-blue-900/30'
|
||||||
|
|
||||||
|
const designerTabActiveClass =
|
||||||
|
'border-blue-700 !bg-blue-700 !text-white shadow-sm dark:border-blue-500 dark:!bg-blue-600 dark:!text-white'
|
||||||
|
|
||||||
|
const designerTabInactiveClass =
|
||||||
|
'border-gray-200 !bg-white !text-slate-600 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:!text-gray-300 dark:hover:!bg-gray-700'
|
||||||
|
|
||||||
export function WorkflowDesigner({
|
export function WorkflowDesigner({
|
||||||
busy,
|
busy,
|
||||||
canvasRef,
|
canvasRef,
|
||||||
|
|
@ -177,10 +195,7 @@ function DesignerToolbar({
|
||||||
type="button"
|
type="button"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
shape="round"
|
shape="round"
|
||||||
className={classNames(
|
className={classNames(designerButtonClass, designerNeutralColorClass)}
|
||||||
designerButtonClass,
|
|
||||||
'border-gray-300 !bg-white text-slate-700 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:text-gray-200 dark:hover:!bg-gray-700',
|
|
||||||
)}
|
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
title="Demo akışı yükle"
|
title="Demo akışı yükle"
|
||||||
onClick={onResetDemo}
|
onClick={onResetDemo}
|
||||||
|
|
@ -192,10 +207,7 @@ function DesignerToolbar({
|
||||||
type="button"
|
type="button"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
shape="round"
|
shape="round"
|
||||||
className={classNames(
|
className={classNames(designerButtonClass, designerAccentColorClass)}
|
||||||
designerButtonClass,
|
|
||||||
'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
|
|
||||||
)}
|
|
||||||
disabled={busy || currentCriteria.length === 0}
|
disabled={busy || currentCriteria.length === 0}
|
||||||
title="Düğümleri okunabilir şekilde yerleştir"
|
title="Düğümleri okunabilir şekilde yerleştir"
|
||||||
onClick={onFitLayout}
|
onClick={onFitLayout}
|
||||||
|
|
@ -207,10 +219,7 @@ function DesignerToolbar({
|
||||||
type="button"
|
type="button"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
shape="round"
|
shape="round"
|
||||||
className={classNames(
|
className={classNames(designerIconButtonClass, designerAccentColorClass)}
|
||||||
designerIconButtonClass,
|
|
||||||
'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
|
|
||||||
)}
|
|
||||||
title="Yakınlaştır"
|
title="Yakınlaştır"
|
||||||
onClick={onZoomIn}
|
onClick={onZoomIn}
|
||||||
>
|
>
|
||||||
|
|
@ -220,10 +229,7 @@ function DesignerToolbar({
|
||||||
type="button"
|
type="button"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
shape="round"
|
shape="round"
|
||||||
className={classNames(
|
className={classNames(designerIconButtonClass, designerAccentColorClass)}
|
||||||
designerIconButtonClass,
|
|
||||||
'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
|
|
||||||
)}
|
|
||||||
title="Uzaklaştır"
|
title="Uzaklaştır"
|
||||||
onClick={onZoomOut}
|
onClick={onZoomOut}
|
||||||
>
|
>
|
||||||
|
|
@ -240,10 +246,7 @@ function DesignerToolbar({
|
||||||
type="button"
|
type="button"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
shape="round"
|
shape="round"
|
||||||
className={classNames(
|
className={classNames(designerButtonClass, designerAccentColorClass)}
|
||||||
designerButtonClass,
|
|
||||||
'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
|
|
||||||
)}
|
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => onAddCriteria(option.value)}
|
onClick={() => onAddCriteria(option.value)}
|
||||||
>
|
>
|
||||||
|
|
@ -274,9 +277,7 @@ function DesignerTabs({
|
||||||
role="tab"
|
role="tab"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
designerTabClass,
|
designerTabClass,
|
||||||
activeTab === 'flow'
|
activeTab === 'flow' ? designerTabActiveClass : designerTabInactiveClass,
|
||||||
? 'border-blue-700 !bg-blue-700 text-white shadow-sm dark:border-blue-500 dark:!bg-blue-600'
|
|
||||||
: 'border-gray-200 !bg-white text-slate-600 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700',
|
|
||||||
)}
|
)}
|
||||||
onClick={() => onChange('flow')}
|
onClick={() => onChange('flow')}
|
||||||
>
|
>
|
||||||
|
|
@ -289,9 +290,7 @@ function DesignerTabs({
|
||||||
role="tab"
|
role="tab"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
designerTabClass,
|
designerTabClass,
|
||||||
activeTab === 'criteria'
|
activeTab === 'criteria' ? designerTabActiveClass : designerTabInactiveClass,
|
||||||
? 'border-blue-700 !bg-blue-700 text-white shadow-sm dark:border-blue-500 dark:!bg-blue-600'
|
|
||||||
: 'border-gray-200 !bg-white text-slate-600 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700',
|
|
||||||
)}
|
)}
|
||||||
onClick={() => onChange('criteria')}
|
onClick={() => onChange('criteria')}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import navigationIcon from '@/proxy/menus/navigation-icon.config'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { FaChevronDown, FaPlus } from 'react-icons/fa'
|
import { FaChevronDown, FaPlus } from 'react-icons/fa'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { useConfig } from '@/components/ui/ConfigProvider'
|
||||||
|
import { useForm } from '@/components/ui/Form/context'
|
||||||
|
|
||||||
const menuService = new MenuService()
|
const menuService = new MenuService()
|
||||||
|
|
||||||
|
|
@ -26,6 +28,12 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
||||||
const [limit, setLimit] = useState(ICON_PAGE_SIZE)
|
const [limit, setLimit] = useState(ICON_PAGE_SIZE)
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Tetikleyici yüksekliği, komşu Input/Select alanlarıyla aynı olsun diye
|
||||||
|
// Input bileşeniyle birebir aynı kaynaktan (form context → global config) okunur.
|
||||||
|
const formSize = useForm()?.size
|
||||||
|
const { controlSize: defaultControlSize } = useConfig()
|
||||||
|
const controlSize = formSize ?? defaultControlSize
|
||||||
|
|
||||||
const SelectedIcon = value ? navigationIcon[value] : null
|
const SelectedIcon = value ? navigationIcon[value] : null
|
||||||
const filtered = search.trim()
|
const filtered = search.trim()
|
||||||
? ALL_ICON_ENTRIES.filter(([key]) => key.toLowerCase().includes(search.toLowerCase()))
|
? ALL_ICON_ENTRIES.filter(([key]) => key.toLowerCase().includes(search.toLowerCase()))
|
||||||
|
|
@ -48,27 +56,33 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={wrapperRef} className="relative">
|
<div ref={wrapperRef} className="relative">
|
||||||
|
{/*
|
||||||
|
Tetikleyici, platformun `.input` sınıfını kullanır; böylece kenarlık, köşe
|
||||||
|
yarıçapı, dolgu ve açık/koyu tema arka planı diğer form alanlarıyla birebir
|
||||||
|
aynı olur. Metin renkleri `!` ile verilir: Button bileşeni variant'ına göre
|
||||||
|
kendi `text-*` sınıfını basıyor ve aynı özgüllükte olduğu için eziyordu.
|
||||||
|
*/}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="none"
|
shape="none"
|
||||||
className={`flex items-center gap-2 w-full !h-auto !justify-start !rounded-lg border !px-3 !py-2 text-sm text-left transition-colors
|
size={controlSize}
|
||||||
${invalid ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
|
className={`input flex items-center gap-2 !justify-start !px-3 text-left text-sm !text-gray-700 dark:!text-gray-100 hover:border-indigo-400
|
||||||
bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:border-indigo-400`}
|
${invalid ? 'input-invalid' : ''}`}
|
||||||
>
|
>
|
||||||
{SelectedIcon ? (
|
{SelectedIcon ? (
|
||||||
<span className="flex items-center gap-2 flex-1 truncate">
|
<span className="flex items-center gap-2 flex-1 truncate">
|
||||||
<SelectedIcon className="text-xl shrink-0" />
|
<SelectedIcon className="text-xl shrink-0" />
|
||||||
<span className="text-xs text-gray-500 truncate">{value}</span>
|
<span className="truncate">{value}</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400 flex-1">
|
<span className="flex-1 !text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step1.SelectIcon') || 'Select icon...'}
|
{translate('::ListForms.Wizard.Step1.SelectIcon') || 'Select icon...'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<FaChevronDown
|
<FaChevronDown
|
||||||
className={`text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`}
|
className={`shrink-0 text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue