ListFormWizarda Custom Component yapısı eklendi
This commit is contained in:
parent
a0121f8e11
commit
7870ebb7bc
18 changed files with 1594 additions and 537 deletions
|
|
@ -6,11 +6,28 @@ namespace Sozsoft.Platform.ListForms;
|
|||
|
||||
public class ListFormWizardDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Wizard'in yol ayrimi. Varsayilan <see cref="WizardComponentKindEnum.List"/> oldugu icin
|
||||
/// bu alani icermeyen eski seed dosyalari mevcut ListForm davranisiyla calismaya devam eder.
|
||||
/// Seed dosyasinda ilk alan olarak yer alir; dosyaya bakan kisi once yolu gorur.
|
||||
/// </summary>
|
||||
public WizardComponentKindEnum ComponentKind { get; set; } = WizardComponentKindEnum.List;
|
||||
|
||||
public string WizardName { get; set; }
|
||||
public string ListFormCode { get; set; }
|
||||
public string MenuCode { get; set; }
|
||||
public int MenuOrder { get; set; }
|
||||
|
||||
/// <summary>Custom yolunda menuye baglanacak CustomComponent'in adi.</summary>
|
||||
public string CustomComponentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Menu kaydina yazilan URL; Custom yolunda ayni zamanda rotanin tek kaynagidir.
|
||||
/// List yolunda <c>/admin/list/{menuCode}</c> olarak hesaplanir, Custom yolunda
|
||||
/// secilen CustomComponent'in RoutePath degeri buraya yazilir.
|
||||
/// </summary>
|
||||
public string MenuUrl { get; set; }
|
||||
|
||||
public bool IsTenant { get; set; }
|
||||
public bool IsBranch { get; set; }
|
||||
public bool IsOrganizationUnit { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Sozsoft.Platform.Enums;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
|
|
@ -56,4 +57,13 @@ public class WizardFileInfoDto
|
|||
public string MenuIcon { get; set; }
|
||||
public string CreatedAt { get; set; }
|
||||
public bool HasInsertedRecords { get; set; }
|
||||
|
||||
/// <summary>Wizard'in List mi Custom Component mi urettigi; listede rozet olarak gosterilir.</summary>
|
||||
public WizardComponentKindEnum ComponentKind { get; set; }
|
||||
|
||||
/// <summary>Custom yolunda bagli CustomComponent adi.</summary>
|
||||
public string CustomComponentName { get; set; }
|
||||
|
||||
/// <summary>Menu kaydina yazilan URL; Custom yolunda bilesenin rota yoludur.</summary>
|
||||
public string MenuUrl { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,11 @@ public class ListFormWizardAppService(
|
|||
input.ListFormCode = listFormCode;
|
||||
input.MenuCode = code;
|
||||
|
||||
// Yol ayrımı: Custom seçildiğinde ListForm ekosistemi (veri kaynağı, ListForm,
|
||||
// alanlar, workflow) hiç üretilmez; menü doğrudan CustomComponent rotasına bağlanır.
|
||||
var isCustomComponent = input.ComponentKind == WizardComponentKindEnum.Custom;
|
||||
var customRoutePath = NormalizeCustomRoutePath(input, isCustomComponent);
|
||||
|
||||
var titleLangKey = $"{listFormCode}.Title";
|
||||
var nameLangKey = code;
|
||||
var descLangKey = $"{listFormCode}.Desc";
|
||||
|
|
@ -95,8 +100,13 @@ public class ListFormWizardAppService(
|
|||
|
||||
//Dil - Language Keys
|
||||
await CreateLangKey(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr, inserted);
|
||||
await CreateLangKey(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr, inserted);
|
||||
await CreateLangKey(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr, inserted);
|
||||
// Title/Desc anahtarları yalnızca ListForm tarafından tüketilir; Custom yolunda
|
||||
// öksüz kayıt bırakmamak için üretilmez.
|
||||
if (!isCustomComponent)
|
||||
{
|
||||
await CreateLangKey(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr, inserted);
|
||||
await CreateLangKey(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr, inserted);
|
||||
}
|
||||
|
||||
//Permission Group
|
||||
var groupName = input.PermissionGroupName ?? PlatformConsts.AppName;
|
||||
|
|
@ -133,19 +143,23 @@ public class ListFormWizardAppService(
|
|||
("Note", WizardConsts.LangKeyNote),
|
||||
};
|
||||
|
||||
// Custom yolunda kayıt üzerinde CRUD yapılmadığı için yalnızca read izni üretilir.
|
||||
var permissionNames = new List<string> { permRead.Name };
|
||||
foreach (var (suffix, langKey) in childPermissions)
|
||||
if (!isCustomComponent)
|
||||
{
|
||||
var permissionName = $"{code}.{suffix}";
|
||||
permissionNames.Add(permissionName);
|
||||
foreach (var (suffix, langKey) in childPermissions)
|
||||
{
|
||||
var permissionName = $"{code}.{suffix}";
|
||||
permissionNames.Add(permissionName);
|
||||
|
||||
if (existingPerms.Any(a => a.Name == permissionName))
|
||||
continue;
|
||||
if (existingPerms.Any(a => a.Name == permissionName))
|
||||
continue;
|
||||
|
||||
await repoPerm.InsertAsync(
|
||||
new PermissionDefinitionRecord(GuidGenerator.Create(), groupName, permissionName, permRead.Name, langKey, true, MultiTenancySides.Both),
|
||||
autoSave: false);
|
||||
inserted.PermissionNames.Add(permissionName);
|
||||
await repoPerm.InsertAsync(
|
||||
new PermissionDefinitionRecord(GuidGenerator.Create(), groupName, permissionName, permRead.Name, langKey, true, MultiTenancySides.Both),
|
||||
autoSave: false);
|
||||
inserted.PermissionNames.Add(permissionName);
|
||||
}
|
||||
}
|
||||
|
||||
// Permission Grants - Bulk Insert (only missing ones)
|
||||
|
|
@ -196,6 +210,11 @@ public class ListFormWizardAppService(
|
|||
var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0;
|
||||
var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1;
|
||||
input.MenuOrder = menuOrder;
|
||||
// Menünün açacağı adres: Custom yolunda CustomComponent'in kendi rotası (bu rota
|
||||
// mapCustomComponentRoutes tarafından zaten kayıtlıdır), aksi halde ListForm adresi.
|
||||
// Seed dosyasından da görülebilmesi için input'a geri yazılır.
|
||||
var menuUrl = isCustomComponent ? customRoutePath : WizardConsts.MenuUrl(code);
|
||||
input.MenuUrl = menuUrl;
|
||||
var existingMenu = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == code));
|
||||
if (existingMenu == null)
|
||||
{
|
||||
|
|
@ -209,18 +228,28 @@ public class ListFormWizardAppService(
|
|||
Target = null,
|
||||
ElementId = null,
|
||||
CssClass = null,
|
||||
Url = WizardConsts.MenuUrl(code),
|
||||
Url = menuUrl,
|
||||
RequiredPermissionName = permRead.Name,
|
||||
Order = menuOrder,
|
||||
}, autoSave: false);
|
||||
inserted.MenuCodes.Add(code);
|
||||
}
|
||||
else if (existingMenu.Order != menuOrder)
|
||||
else if (existingMenu.Order != menuOrder || existingMenu.Url != menuUrl)
|
||||
{
|
||||
existingMenu.Order = menuOrder;
|
||||
existingMenu.Url = menuUrl;
|
||||
await repoMenu.UpdateAsync(existingMenu, autoSave: false);
|
||||
}
|
||||
|
||||
// Custom yolunda ListForm ekosistemi üretilmez: veri kaynağı, ListForm, alanlar,
|
||||
// subform/widget/workflow kayıtlarının hiçbirine ihtiyaç yoktur.
|
||||
if (isCustomComponent)
|
||||
{
|
||||
await _languageTextAppService.ClearRedisCacheAsync();
|
||||
await SaveWizardSeedFileAsync(input, false, false, inserted);
|
||||
return;
|
||||
}
|
||||
|
||||
//DataSource kodu ile iligli kod blogu
|
||||
var dataSourceQueryable = await repoDataSource.GetQueryableAsync();
|
||||
var dataSource = await AsyncExecuter.FirstOrDefaultAsync(dataSourceQueryable.Where(a => a.Code == input.DataSourceCode));
|
||||
|
|
@ -572,6 +601,36 @@ public class ListFormWizardAppService(
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom yolunda component adı ve rota zorunludur. Rota <see cref="ListFormWizardDto.MenuUrl"/>
|
||||
/// üzerinden taşınır ve CustomComponent sözleşmesiyle (<c>CreateUpdateCustomComponentDto.RoutePath</c>)
|
||||
/// aynı şekilde '/' ile başlamalıdır; aksi halde menü, router'a kayıtlı olmayan bir adrese bağlanır.
|
||||
/// List yolunda bileşen adı temizlenerek seed dosyasında artık veri bırakılmaz.
|
||||
/// </summary>
|
||||
private static string? NormalizeCustomRoutePath(ListFormWizardDto input, bool isCustomComponent)
|
||||
{
|
||||
if (!isCustomComponent)
|
||||
{
|
||||
input.CustomComponentName = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
var componentName = input.CustomComponentName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(componentName))
|
||||
throw new UserFriendlyException("Custom Component seçimi zorunludur.");
|
||||
|
||||
var routePath = input.MenuUrl?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(routePath))
|
||||
throw new UserFriendlyException($"'{componentName}' bileşeninin rota yolu (Route Path) boş olamaz.");
|
||||
|
||||
if (!routePath.StartsWith('/'))
|
||||
routePath = "/" + routePath;
|
||||
|
||||
input.CustomComponentName = componentName;
|
||||
input.MenuUrl = routePath;
|
||||
return routePath;
|
||||
}
|
||||
|
||||
public async Task<List<WizardFileInfoDto>> GetFiles()
|
||||
{
|
||||
var outputPath = ResolveWizardSeedOutputPath();
|
||||
|
|
@ -594,6 +653,9 @@ public class ListFormWizardAppService(
|
|||
DefaultLayout = seed?.Wizard?.DefaultLayout ?? string.Empty,
|
||||
MenuIcon = seed?.Wizard?.MenuIcon ?? string.Empty,
|
||||
ListFormCode = seed?.Wizard?.ListFormCode ?? string.Empty,
|
||||
ComponentKind = seed?.Wizard?.ComponentKind ?? WizardComponentKindEnum.List,
|
||||
CustomComponentName = seed?.Wizard?.CustomComponentName ?? string.Empty,
|
||||
MenuUrl = seed?.Wizard?.MenuUrl ?? string.Empty,
|
||||
CreatedAt = fileName.Length >= 12 ? fileName[..12] : fileName,
|
||||
HasInsertedRecords = seed?.InsertedRecords != null &&
|
||||
(seed.InsertedRecords.LanguageKeys.Count > 0 ||
|
||||
|
|
@ -675,8 +737,10 @@ public class ListFormWizardAppService(
|
|||
var listFormCode = seed.Wizard?.ListFormCode;
|
||||
var menuParentCode = seed.Wizard?.MenuParentCode;
|
||||
|
||||
// ListForm ve alanları her zaman sil (wizard bunları her zaman oluşturur)
|
||||
if (!string.IsNullOrWhiteSpace(listFormCode))
|
||||
// ListForm ve alanlarını sil. Custom yolunda wizard hiç ListForm üretmediği için
|
||||
// bu blok atlanır; aksi halde aynı kodu taşıyan bir List wizard'ının ListForm'u silinirdi.
|
||||
var isCustomComponent = seed.Wizard?.ComponentKind == WizardComponentKindEnum.Custom;
|
||||
if (!isCustomComponent && !string.IsNullOrWhiteSpace(listFormCode))
|
||||
{
|
||||
var lf = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
||||
if (lf != null) await repoListForm.DeleteAsync(lf, autoSave: true);
|
||||
|
|
|
|||
|
|
@ -281,6 +281,73 @@ public class MenuAppService : CrudAppService<
|
|||
return entityDtos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Menüyü tüm alanlarıyla günceller ve görünen adın dil metinlerini de yazar.
|
||||
/// Kod (Code) dil anahtarı ve yetki eşleşmesi olduğu için değiştirilemez; gönderilen
|
||||
/// değer yok sayılıp mevcut kod korunur.
|
||||
/// </summary>
|
||||
public async Task<MenuDto> UpdateWithLanguageKeyTextAsync(Guid id, MenuDto input)
|
||||
{
|
||||
await CheckUpdatePolicyAsync();
|
||||
|
||||
var menu = await _menuRepository.GetAsync(id);
|
||||
|
||||
// Kod değiştirilemez: dil anahtarı, alt menülerin ParentCode'u ve permission
|
||||
// kayıtları bu değere bağlıdır.
|
||||
input.Code = menu.Code;
|
||||
|
||||
await UpsertMenuLanguageTextsAsync(menu.Code, input.MenuTextEn, input.MenuTextTr);
|
||||
|
||||
return await base.UpdateAsync(id, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Menü koduna karşılık gelen dil anahtarını ve EN/TR metinlerini oluşturur veya günceller.
|
||||
/// </summary>
|
||||
private async Task UpsertMenuLanguageTextsAsync(string code, string menuTextEn, string menuTextTr)
|
||||
{
|
||||
var keyExists = await _repositoryKey.AnyAsync(
|
||||
a => a.Key == code &&
|
||||
a.ResourceName == PlatformConsts.AppName);
|
||||
if (!keyExists)
|
||||
{
|
||||
await _repositoryKey.InsertAsync(new LanguageKey
|
||||
{
|
||||
Key = code,
|
||||
ResourceName = PlatformConsts.AppName
|
||||
}, autoSave: true);
|
||||
}
|
||||
|
||||
foreach (var (culture, value) in new[] { ("en", menuTextEn), ("tr", menuTextTr) })
|
||||
{
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
var existing = await _repositoryText.FirstOrDefaultAsync(
|
||||
a => a.CultureName == culture &&
|
||||
a.Key == code &&
|
||||
a.ResourceName == PlatformConsts.AppName);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Value = value;
|
||||
await _repositoryText.UpdateAsync(existing, autoSave: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _repositoryText.InsertAsync(new LanguageText
|
||||
{
|
||||
Key = code,
|
||||
CultureName = culture,
|
||||
Value = value,
|
||||
ResourceName = PlatformConsts.AppName
|
||||
}, autoSave: true);
|
||||
}
|
||||
}
|
||||
|
||||
await _languageTextAppService.ClearRedisCacheAsync();
|
||||
}
|
||||
|
||||
public async Task<MenuDto> CreateWithLanguageKeyTextAsync(MenuDto input)
|
||||
{
|
||||
await CheckCreatePolicyAsync();
|
||||
|
|
|
|||
|
|
@ -16254,6 +16254,12 @@
|
|||
"en": "Clear All",
|
||||
"tr": "Tümünü Temizle"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStep2.DeployUrl",
|
||||
"en": "Deploy URL",
|
||||
"tr": "Yayınlama URL'si"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStep2.ListFormCodeHint",
|
||||
|
|
@ -16674,6 +16680,120 @@
|
|||
"en": "Deploying to server…",
|
||||
"tr": "Sunucuya deploy ediliyor…"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Step4Log.LinkingCustomComponent",
|
||||
"en": "Linking custom component '{0}'…",
|
||||
"tr": "'{0}' özel bileşeni bağlanıyor…"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Platform.EditMenu",
|
||||
"en": "Edit Menu",
|
||||
"tr": "Menüyü Düzenle"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Platform.Status",
|
||||
"en": "Status",
|
||||
"tr": "Durum"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStep1.RootMenu",
|
||||
"en": "Root menu (no parent)",
|
||||
"tr": "Kök menü (üst menü yok)"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.MenuUrl",
|
||||
"en": "Menu URL",
|
||||
"tr": "Menü URL"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.MenuUrlFromComponent",
|
||||
"en": "Comes from the selected custom component",
|
||||
"tr": "Seçilen özel bileşenden geliyor"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.MenuUrlFromList",
|
||||
"en": "Derived from the list form to be generated",
|
||||
"tr": "Üretilecek liste formundan türetiliyor"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.ComponentType",
|
||||
"en": "Component Type",
|
||||
"tr": "Bileşen Tipi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.ComponentTypeHint",
|
||||
"en": "Decides whether the wizard generates a list form or links an existing custom component",
|
||||
"tr": "Wizard'ın liste formu mu üreteceğini yoksa mevcut bir özel bileşene mi bağlanacağını belirler"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.ListComponent",
|
||||
"en": "List Component",
|
||||
"tr": "Liste Bileşeni"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.ListComponentHint",
|
||||
"en": "Generates the platform list form: data source, fields, sub forms, widgets and workflow",
|
||||
"tr": "Platform liste formunu üretir: veri kaynağı, alanlar, alt formlar, widget'lar ve iş akışı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.CustomComponent",
|
||||
"en": "Custom Component",
|
||||
"tr": "Özel Bileşen"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.CustomComponentHint",
|
||||
"en": "Links the menu to a component built in Developer Kit; no list form is generated",
|
||||
"tr": "Menüyü Developer Kit'te üretilmiş bir bileşene bağlar; liste formu üretilmez"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.ComponentName",
|
||||
"en": "Component Name",
|
||||
"tr": "Bileşen Adı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.SelectComponent",
|
||||
"en": "Select component…",
|
||||
"tr": "Bileşen seçin…"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.RoutePath",
|
||||
"en": "Route Path",
|
||||
"tr": "Rota Yolu"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.RoutePathHint",
|
||||
"en": "The menu item created after deploy opens this address",
|
||||
"tr": "Deploy sonrası oluşan menü bu adresi açar"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.Preview",
|
||||
"en": "Preview",
|
||||
"tr": "Önizleme"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.WizardStepComponent.NoComponents",
|
||||
"en": "No active custom component with a route path was found. Create one in Developer Kit → Components first.",
|
||||
"tr": "Rota yolu tanımlı aktif bir özel bileşen bulunamadı. Önce Developer Kit → Bileşenler ekranından oluşturun."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Step4Log.Completed",
|
||||
|
|
|
|||
|
|
@ -115,8 +115,13 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
|
||||
await BackfillMenuParentShortNameAsync(seedFile.Wizard);
|
||||
|
||||
// Zaten seeded mi kontrol et (ListForm var mı?)
|
||||
if (await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode))
|
||||
// Zaten seeded mi kontrol et. Custom yolunda ListForm üretilmediği için
|
||||
// idempotency menü kaydı üzerinden değerlendirilir.
|
||||
var alreadySeeded = seedFile.Wizard.ComponentKind == WizardComponentKindEnum.Custom
|
||||
? await _repoMenu.AnyAsync(a => a.Code == ResolveMenuCode(seedFile.Wizard))
|
||||
: await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode);
|
||||
|
||||
if (alreadySeeded)
|
||||
{
|
||||
_logger.LogInformation("[{File}] '{WizardName}' already exists, skipped.", fileName, wizardName);
|
||||
continue;
|
||||
|
|
@ -163,10 +168,17 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
var permImportName = $"{code}.Import";
|
||||
var permNoteName = $"{code}.Note";
|
||||
|
||||
// Yol ayrımı: Custom yolunda ListForm ekosistemi üretilmez (AppService.Create ile aynı davranış).
|
||||
var isCustomComponent = input.ComponentKind == WizardComponentKindEnum.Custom;
|
||||
var customRoutePath = NormalizeSeedCustomRoutePath(input);
|
||||
|
||||
// Dil - Language Keys
|
||||
await CreateLangKeyAsync(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr);
|
||||
await CreateLangKeyAsync(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr);
|
||||
await CreateLangKeyAsync(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr);
|
||||
if (!isCustomComponent)
|
||||
{
|
||||
await CreateLangKeyAsync(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr);
|
||||
await CreateLangKeyAsync(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr);
|
||||
}
|
||||
|
||||
// Permission Group
|
||||
var groupName = input.PermissionGroupName ?? AppName;
|
||||
|
|
@ -189,35 +201,33 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
permRead = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both), autoSave: true);
|
||||
|
||||
var permCreate = existingPerms.FirstOrDefault(a => a.Name == permCreateName);
|
||||
if (permCreate == null)
|
||||
permCreate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true);
|
||||
// Custom yolunda kayıt üzerinde CRUD yapılmadığı için yalnızca read izni üretilir.
|
||||
if (!isCustomComponent)
|
||||
{
|
||||
if (!existingPerms.Any(a => a.Name == permCreateName))
|
||||
await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true);
|
||||
|
||||
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == permUpdateName);
|
||||
if (permUpdate == null)
|
||||
permUpdate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true);
|
||||
if (!existingPerms.Any(a => a.Name == permUpdateName))
|
||||
await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true);
|
||||
|
||||
var permDelete = existingPerms.FirstOrDefault(a => a.Name == permDeleteName);
|
||||
if (permDelete == null)
|
||||
permDelete = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true);
|
||||
if (!existingPerms.Any(a => a.Name == permDeleteName))
|
||||
await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true);
|
||||
|
||||
var permExport = existingPerms.FirstOrDefault(a => a.Name == permExportName);
|
||||
if (permExport == null)
|
||||
permExport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true);
|
||||
if (!existingPerms.Any(a => a.Name == permExportName))
|
||||
await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true);
|
||||
|
||||
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: true);
|
||||
if (!existingPerms.Any(a => a.Name == permImportName))
|
||||
await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true);
|
||||
|
||||
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: true);
|
||||
if (!existingPerms.Any(a => a.Name == permNoteName))
|
||||
await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||
Guid.NewGuid(), groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true);
|
||||
}
|
||||
|
||||
// // Permission Grants - Admin role için, sadece eksik olanları ekle
|
||||
// var existingGrants = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName);
|
||||
|
|
@ -264,6 +274,14 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0;
|
||||
var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1;
|
||||
input.MenuOrder = menuOrder;
|
||||
// Menünün açacağı adres. Custom yolunda normalize edilmis rota, List yolunda
|
||||
// MenuUrl alani (tasimayan eski dosyalar icin AppService.Create ile aynı kural).
|
||||
var menuUrl = isCustomComponent
|
||||
? customRoutePath
|
||||
: !string.IsNullOrWhiteSpace(input.MenuUrl)
|
||||
? input.MenuUrl.Trim()
|
||||
: WizardConsts.MenuUrl(code);
|
||||
input.MenuUrl = menuUrl;
|
||||
var existingMenu = await _repoMenu.FirstOrDefaultAsync(a => a.Code == code);
|
||||
if (existingMenu == null)
|
||||
{
|
||||
|
|
@ -277,17 +295,22 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
Target = null,
|
||||
ElementId = null,
|
||||
CssClass = null,
|
||||
Url = WizardConsts.MenuUrl(code),
|
||||
Url = menuUrl,
|
||||
RequiredPermissionName = permRead.Name,
|
||||
Order = menuOrder,
|
||||
}, autoSave: true);
|
||||
}
|
||||
else if (existingMenu.Order != menuOrder)
|
||||
else if (existingMenu.Order != menuOrder || existingMenu.Url != menuUrl)
|
||||
{
|
||||
existingMenu.Order = menuOrder;
|
||||
existingMenu.Url = menuUrl;
|
||||
await _repoMenu.UpdateAsync(existingMenu, autoSave: true);
|
||||
}
|
||||
|
||||
// Custom yolunda ListForm ekosistemi üretilmez.
|
||||
if (isCustomComponent)
|
||||
return;
|
||||
|
||||
// DataSource
|
||||
var existingDataSource = await _repoDataSource.FirstOrDefaultAsync(a => a.Code == input.DataSourceCode);
|
||||
if (existingDataSource == null)
|
||||
|
|
@ -527,6 +550,34 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wizard'in menu kodunu, ApplyWizardSeedAsync ile ayni kurala gore cozer.
|
||||
/// Custom yolunda idempotency kontrolu ListForm yerine bu kod uzerinden yapilir.
|
||||
/// </summary>
|
||||
private static string ResolveMenuCode(ListFormWizardDto input)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(input.MenuCode)
|
||||
? WizardConsts.WizardKey(input.WizardName.Trim())
|
||||
: input.MenuCode.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom yolunda menunun acacagi rotayi normalize eder. Rota MenuUrl uzerinden
|
||||
/// tasinir; bos ise menu hicbir yere gitmeyecegi icin seed dosyasi hatali kabul edilir.
|
||||
/// </summary>
|
||||
private static string NormalizeSeedCustomRoutePath(ListFormWizardDto input)
|
||||
{
|
||||
if (input.ComponentKind != WizardComponentKindEnum.Custom)
|
||||
return null;
|
||||
|
||||
var routePath = input.MenuUrl?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(routePath))
|
||||
throw new InvalidOperationException(
|
||||
$"'{input.WizardName}' custom component wizard'inda Route Path bos olamaz.");
|
||||
|
||||
return routePath.StartsWith('/') ? routePath : "/" + routePath;
|
||||
}
|
||||
|
||||
private async Task BackfillMenuParentShortNameAsync(ListFormWizardDto input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.MenuParentCode) ||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
namespace Sozsoft.Platform.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Wizard'in hangi yolu urettigini belirler. Varsayilan <see cref="List"/> oldugu icin
|
||||
/// bu alani icermeyen eski wizard seed dosyalari da mevcut davranisla calismaya devam eder.
|
||||
/// </summary>
|
||||
public enum WizardComponentKindEnum
|
||||
{
|
||||
/// <summary>Platformun dinamik ListForm ekosistemi uretilir (varsayilan).</summary>
|
||||
List = 0,
|
||||
|
||||
/// <summary>DeveloperKit'te uretilmis bir CustomComponent'e menu baglanir; ListForm uretilmez.</summary>
|
||||
Custom = 1,
|
||||
}
|
||||
36
configs/seeds/host/crud/AbpRoles.json
Normal file
36
configs/seeds/host/crud/AbpRoles.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"EntityName": "AbpRoles",
|
||||
"GeneratedAt": "2026-08-18T07:26:49.511788Z",
|
||||
"Endpoints": [
|
||||
{
|
||||
"Method": "POST",
|
||||
"Path": "/api/app/crudendpoint/AbpRoles",
|
||||
"OperationType": "Create",
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Method": "DELETE",
|
||||
"Path": "/api/app/crudendpoint/AbpRoles/{id}",
|
||||
"OperationType": "Delete",
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Method": "GET",
|
||||
"Path": "/api/app/crudendpoint/AbpRoles/{id}",
|
||||
"OperationType": "GetById",
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Method": "GET",
|
||||
"Path": "/api/app/crudendpoint/AbpRoles",
|
||||
"OperationType": "GetList",
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Method": "PUT",
|
||||
"Path": "/api/app/crudendpoint/AbpRoles/{id}",
|
||||
"OperationType": "Update",
|
||||
"IsActive": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -27,11 +27,23 @@ export interface ListFormWizardColumnGroupDto {
|
|||
items: ListFormWizardColumnItemDto[]
|
||||
}
|
||||
|
||||
/** Wizard'ın yol ayrımı; backend `WizardComponentKindEnum` ile birebir eşleşir. */
|
||||
export enum WizardComponentKindEnum {
|
||||
List = 0,
|
||||
Custom = 1,
|
||||
}
|
||||
|
||||
export interface ListFormWizardDto {
|
||||
componentKind: WizardComponentKindEnum
|
||||
|
||||
wizardName: string
|
||||
listFormCode: string
|
||||
menuCode: string
|
||||
menuOrder: number
|
||||
|
||||
customComponentName: string
|
||||
/** Menünün açtığı adres; Custom yolunda seçilen bileşenin rota yolu. */
|
||||
menuUrl: string
|
||||
isTenant: boolean
|
||||
isBranch: boolean
|
||||
isOrganizationUnit: boolean
|
||||
|
|
@ -90,6 +102,9 @@ export interface WizardFileInfoDto {
|
|||
menuIcon: string
|
||||
createdAt: string
|
||||
hasInsertedRecords: boolean
|
||||
componentKind: WizardComponentKindEnum
|
||||
customComponentName: string
|
||||
menuUrl: string
|
||||
}
|
||||
|
||||
export interface WizardInsertedRecordsDto {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,17 @@ export class MenuService {
|
|||
{ apiName: this.apiName },
|
||||
)
|
||||
|
||||
/** Menüyü tüm alanlarıyla günceller; görünen adın EN/TR dil metinlerini de yazar. */
|
||||
updateWithLanguageKeyText = (id: string, input: MenuDto) =>
|
||||
apiService.fetchData<MenuDto, MenuDto>(
|
||||
{
|
||||
method: 'PUT',
|
||||
url: `/api/app/menu/${id}/with-language-key-text`,
|
||||
data: input,
|
||||
},
|
||||
{ apiName: this.apiName },
|
||||
)
|
||||
|
||||
createWithLanguageKeyText = (input: MenuDto) =>
|
||||
apiService.fetchData<MenuDto, MenuDto>(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ import { Container } from '@/components/shared'
|
|||
import { sqlDataTypeToDbType } from '../edit/options'
|
||||
import { useStoreActions } from '@/store/store'
|
||||
import { deleteWizardFile, getWizardFile, postListFormWizard } from '@/services/wizard.service'
|
||||
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
|
||||
import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models'
|
||||
import WizardStepComponent from './WizardStepComponent'
|
||||
import WizardStepCustomComponent from './WizardStepCustomComponent'
|
||||
import {
|
||||
ListFormWorkflowCriteriaDto,
|
||||
SubFormDto,
|
||||
|
|
@ -52,6 +54,9 @@ const initialValues: ListFormWizardDto = {
|
|||
listFormCode: '',
|
||||
menuCode: '',
|
||||
menuOrder: 0,
|
||||
componentKind: WizardComponentKindEnum.List,
|
||||
customComponentName: '',
|
||||
menuUrl: '',
|
||||
isTenant: false,
|
||||
isBranch: false,
|
||||
isOrganizationUnit: false,
|
||||
|
|
@ -174,18 +179,29 @@ const step1ValidationSchema = Yup.object().shape({
|
|||
languageTextMenuTr: Yup.string().required(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Custom Component yolunda Settings adımı hiç gösterilmez; bu alanların zorunluluğu
|
||||
* da kalkmalıdır, aksi halde deploy'a kadar temizlenemeyen hatalar taşınır.
|
||||
*/
|
||||
const requiredForListComponent = <T extends Yup.Schema>(schema: T) =>
|
||||
schema.when('componentKind', {
|
||||
is: (kind: number) => kind !== WizardComponentKindEnum.Custom,
|
||||
then: (s: T) => s.required(),
|
||||
otherwise: (s: T) => s.optional(),
|
||||
})
|
||||
|
||||
const step2ValidationSchema = Yup.object().shape({
|
||||
listFormCode: Yup.string().required(),
|
||||
languageTextTitleEn: Yup.string().required(),
|
||||
languageTextTitleTr: Yup.string().required(),
|
||||
languageTextDescEn: Yup.string().required(),
|
||||
languageTextDescTr: Yup.string().required(),
|
||||
dataSourceCode: Yup.string().required(),
|
||||
listFormCode: requiredForListComponent(Yup.string()),
|
||||
languageTextTitleEn: requiredForListComponent(Yup.string()),
|
||||
languageTextTitleTr: requiredForListComponent(Yup.string()),
|
||||
languageTextDescEn: requiredForListComponent(Yup.string()),
|
||||
languageTextDescTr: requiredForListComponent(Yup.string()),
|
||||
dataSourceCode: requiredForListComponent(Yup.string()),
|
||||
dataSourceConnectionString: Yup.string(),
|
||||
selectCommandType: Yup.string().required(),
|
||||
selectCommand: Yup.string().required(),
|
||||
keyFieldName: Yup.string().required(),
|
||||
keyFieldDbSourceType: Yup.number().required(),
|
||||
selectCommandType: requiredForListComponent(Yup.string()),
|
||||
selectCommand: requiredForListComponent(Yup.string()),
|
||||
keyFieldName: requiredForListComponent(Yup.string()),
|
||||
keyFieldDbSourceType: requiredForListComponent(Yup.number()),
|
||||
isTenant: Yup.boolean(),
|
||||
isBranch: Yup.boolean(),
|
||||
isOrganizationUnit: Yup.boolean(),
|
||||
|
|
@ -225,34 +241,57 @@ const removeTenantGroupItems = (groups: WizardGroup[]) =>
|
|||
items: group.items.filter((item) => !isTenantColumn(item.fieldName)),
|
||||
}))
|
||||
|
||||
/** Yeni wizard açıldığında ön seçilen veri kaynağı kodu. */
|
||||
const DEFAULT_DATA_SOURCE_CODE = 'Default'
|
||||
|
||||
// Auto-derive listFormCode from wizardName
|
||||
const deriveListFormCode = (name: string) => {
|
||||
const sanitized = name.replace(/\s/g, '')
|
||||
return sanitized ? `App.Wizard.${sanitized}` : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Adım kimlikleri. `resolvedStep` geri düşerken `id < currentStep` karşılaştırması
|
||||
* yaptığı için kimlikler görüntüleme sırasıyla artan olmak zorundadır.
|
||||
*/
|
||||
const STEP = {
|
||||
component: 0,
|
||||
customComponent: 1,
|
||||
menu: 2,
|
||||
settings: 3,
|
||||
fields: 4,
|
||||
subForms: 5,
|
||||
widgets: 6,
|
||||
workflow: 7,
|
||||
todo: 8,
|
||||
tree: 9,
|
||||
gantt: 10,
|
||||
scheduler: 11,
|
||||
deploy: 12,
|
||||
} as const
|
||||
|
||||
/** Yalnızca ilgili layout seçiliyken gösterilen adımlar. */
|
||||
const OPTIONAL_STEPS = [
|
||||
{
|
||||
id: 6,
|
||||
id: STEP.todo,
|
||||
flag: 'todo',
|
||||
translationKey: '::App.Platform.TabTodo',
|
||||
fallbackTitle: 'Todo / Kanban',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
id: STEP.tree,
|
||||
flag: 'tree',
|
||||
translationKey: '::App.Platform.TabTree',
|
||||
fallbackTitle: 'Tree',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
id: STEP.gantt,
|
||||
flag: 'gantt',
|
||||
translationKey: '::App.Platform.TabGantt',
|
||||
fallbackTitle: 'Gantt',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
id: STEP.scheduler,
|
||||
flag: 'scheduler',
|
||||
translationKey: '::App.Platform.TabScheduler',
|
||||
fallbackTitle: 'Scheduler',
|
||||
|
|
@ -445,9 +484,23 @@ const Wizard = () => {
|
|||
setIsLoadingDataSource(true)
|
||||
const response = await getDataSources()
|
||||
if (response.data?.items) {
|
||||
setDataSourceList(
|
||||
response.data.items.map((item: any) => ({ value: item.code, label: item.code })),
|
||||
)
|
||||
const options: SelectBoxOption[] = response.data.items.map((item: any) => ({
|
||||
value: item.code,
|
||||
label: item.code,
|
||||
}))
|
||||
setDataSourceList(options)
|
||||
|
||||
// Yeni wizard'da veri kaynağı elle seçilmesin; platform varsayılanı otomatik gelir.
|
||||
// Edit modunda kayıtlı değer sonradan yükleniyor, burada dokunulmaz.
|
||||
if (!isEditMode && !formikRef.current?.values.dataSourceCode) {
|
||||
const defaultOption = options.find(
|
||||
(o) => String(o.value).toLowerCase() === DEFAULT_DATA_SOURCE_CODE.toLowerCase(),
|
||||
)
|
||||
if (defaultOption) {
|
||||
formikRef.current?.setFieldValue('dataSourceCode', defaultOption.value)
|
||||
setCurrentDataSource(String(defaultOption.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsLoadingDataSource(false)
|
||||
}
|
||||
|
|
@ -512,6 +565,10 @@ const Wizard = () => {
|
|||
wizardName: w.wizardName ?? '',
|
||||
listFormCode: w.listFormCode ?? '',
|
||||
menuCode: w.menuCode ?? '',
|
||||
// Bu alanları içermeyen eski seed dosyaları List yolunda açılır.
|
||||
componentKind: w.componentKind ?? WizardComponentKindEnum.List,
|
||||
customComponentName: w.customComponentName ?? '',
|
||||
menuUrl: w.menuUrl ?? '',
|
||||
menuOrder:
|
||||
w.menuOrder && w.menuOrder > 0
|
||||
? w.menuOrder
|
||||
|
|
@ -768,10 +825,30 @@ const Wizard = () => {
|
|||
setCurrentStep(targetStep)
|
||||
}
|
||||
|
||||
const handleNext = () => validateAndGo(step1FieldNames, 1)
|
||||
const handleNext2 = () => validateAndGo(step2FieldNames, 2)
|
||||
/** Menu adimindan sonra List yolunda Settings, Custom yolunda dogrudan Deploy gelir. */
|
||||
const handleMenuNext = (isCustom: boolean) =>
|
||||
validateAndGo(step1FieldNames, isCustom ? STEP.deploy : STEP.settings)
|
||||
const handleNext2 = () => validateAndGo(step2FieldNames, STEP.fields)
|
||||
|
||||
const handleBack = () => setCurrentStep(0)
|
||||
const handleBack = () => setCurrentStep(STEP.menu)
|
||||
|
||||
/**
|
||||
* Yol ayrımı değişince karşı yolun taşıdığı veriler temizlenir; aksi halde
|
||||
* List'ten Custom'a geçip geri dönen kullanıcı, seed dosyasına artık alan yazar.
|
||||
*/
|
||||
const handleComponentKindChange = (kind: WizardComponentKindEnum) => {
|
||||
const formik = formikRef.current
|
||||
formik?.setFieldValue('componentKind', kind)
|
||||
if (kind !== WizardComponentKindEnum.Custom) {
|
||||
formik?.setFieldValue('customComponentName', '')
|
||||
formik?.setFieldValue('menuUrl', '')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCustomComponentChange = (name: string, routePath: string) => {
|
||||
formikRef.current?.setFieldValue('customComponentName', name)
|
||||
formikRef.current?.setFieldValue('menuUrl', routePath)
|
||||
}
|
||||
const { getConfig } = useStoreActions((a) => a.abpConfig)
|
||||
|
||||
const preventEnterSubmit = (event: KeyboardEvent<HTMLFormElement>) => {
|
||||
|
|
@ -792,6 +869,7 @@ const Wizard = () => {
|
|||
if (!formik) throw new Error(translate('::App.Platform.Unknown'))
|
||||
|
||||
const values = formik.values
|
||||
const isCustom = values.componentKind === WizardComponentKindEnum.Custom
|
||||
|
||||
try {
|
||||
// Edit modunda: önce eski dosyayı sil (DB kayıtlarını temizler)
|
||||
|
|
@ -799,6 +877,28 @@ const Wizard = () => {
|
|||
await deleteWizardFile(editFileName)
|
||||
}
|
||||
|
||||
// Custom yolunda ListForm'a ait hiçbir koleksiyon gönderilmez.
|
||||
if (isCustom) {
|
||||
await postListFormWizard({
|
||||
...values,
|
||||
groups: [],
|
||||
subForms: [],
|
||||
widgets: [],
|
||||
})
|
||||
|
||||
await getConfig(true)
|
||||
|
||||
toast.push(
|
||||
<Notification type="success" duration={2000}>
|
||||
{translate('::App.Platform.FormBilgileriKaydedildi')}
|
||||
</Notification>,
|
||||
{ placement: 'bottom-end' },
|
||||
)
|
||||
|
||||
navigate(values.menuUrl, { replace: true })
|
||||
return
|
||||
}
|
||||
|
||||
await postListFormWizard({
|
||||
...values,
|
||||
groups: editingGroups.map((g) => ({
|
||||
|
|
@ -880,33 +980,53 @@ const Wizard = () => {
|
|||
}}
|
||||
>
|
||||
{({ touched, errors, values }) => {
|
||||
// Custom Component yolunda ListForm'a özgü adımların (Settings, Fields,
|
||||
// SubForms, Widgets, Workflow ve layout adımları) hiçbirine ihtiyaç yok.
|
||||
const isCustomComponent = values.componentKind === WizardComponentKindEnum.Custom
|
||||
|
||||
const activeSteps = [
|
||||
{ id: 0, title: translate('::App.VideoRoom.Menu') },
|
||||
{
|
||||
id: 1,
|
||||
title: translate('::App.Setting'),
|
||||
id: STEP.component,
|
||||
title: translate('::App.WizardStepComponent.ComponentType'),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: translate('::App.ListFormWizard.ListFormFields'),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: translate('::App.ListFormEdit.SubForms'),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: translate('::App.ListFormEdit.TabWidgets'),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: translate('::App.ListFormEdit.TabWorkflow'),
|
||||
},
|
||||
...OPTIONAL_STEPS.filter((step) => values[step.flag]).map((step) => ({
|
||||
id: step.id,
|
||||
title: translate(step.translationKey) || step.fallbackTitle,
|
||||
})),
|
||||
{ id: 10, title: translate('::App.Platform.Deploy') },
|
||||
...(isCustomComponent
|
||||
? [
|
||||
{
|
||||
id: STEP.customComponent,
|
||||
title: translate('::App.WizardStepComponent.CustomComponent'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ id: STEP.menu, title: translate('::App.VideoRoom.Menu') },
|
||||
...(isCustomComponent
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: STEP.settings,
|
||||
title: translate('::App.Setting'),
|
||||
},
|
||||
{
|
||||
id: STEP.fields,
|
||||
title: translate('::App.ListFormWizard.ListFormFields'),
|
||||
},
|
||||
{
|
||||
id: STEP.subForms,
|
||||
title: translate('::App.ListFormEdit.SubForms'),
|
||||
},
|
||||
{
|
||||
id: STEP.widgets,
|
||||
title: translate('::App.ListFormEdit.TabWidgets'),
|
||||
},
|
||||
{
|
||||
id: STEP.workflow,
|
||||
title: translate('::App.ListFormEdit.TabWorkflow'),
|
||||
},
|
||||
...OPTIONAL_STEPS.filter((step) => values[step.flag]).map((step) => ({
|
||||
id: step.id,
|
||||
title: translate(step.translationKey) || step.fallbackTitle,
|
||||
})),
|
||||
]),
|
||||
{ id: STEP.deploy, title: translate('::App.Platform.Deploy') },
|
||||
]
|
||||
|
||||
// Opsiyonel bir adım (todo/tree/gantt/scheduler) sonradan kapatılırsa
|
||||
|
|
@ -946,8 +1066,35 @@ const Wizard = () => {
|
|||
|
||||
<Form onKeyDown={preventEnterSubmit}>
|
||||
<FormContainer size={resolvedStep >= 2 ? undefined : 'sm'}>
|
||||
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
||||
{resolvedStep === 0 && (
|
||||
{/* ─── Step 1: Component Type (yol ayrımı) ─────────────── */}
|
||||
{resolvedStep === STEP.component && (
|
||||
<WizardStepComponent
|
||||
values={values}
|
||||
onKindChange={handleComponentKindChange}
|
||||
translate={translate}
|
||||
onNext={() =>
|
||||
setCurrentStep(
|
||||
values.componentKind === WizardComponentKindEnum.Custom
|
||||
? STEP.customComponent
|
||||
: STEP.menu,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 2: Custom Component seçimi ─────────────────── */}
|
||||
{resolvedStep === STEP.customComponent && (
|
||||
<WizardStepCustomComponent
|
||||
values={values}
|
||||
onCustomComponentChange={handleCustomComponentChange}
|
||||
translate={translate}
|
||||
onBack={() => setCurrentStep(STEP.component)}
|
||||
onNext={() => setCurrentStep(STEP.menu)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 3: Menu ───────────────────────────────────── */}
|
||||
{resolvedStep === STEP.menu && (
|
||||
<WizardStep1
|
||||
values={values}
|
||||
errors={errors}
|
||||
|
|
@ -967,13 +1114,22 @@ const Wizard = () => {
|
|||
onReloadMenu={getMenuList}
|
||||
permissionGroupList={permissionGroupList}
|
||||
isLoadingPermissionGroup={isLoadingPermissionGroup}
|
||||
onNext={handleNext}
|
||||
onBack={() =>
|
||||
setCurrentStep(
|
||||
values.componentKind === WizardComponentKindEnum.Custom
|
||||
? STEP.customComponent
|
||||
: STEP.component,
|
||||
)
|
||||
}
|
||||
onNext={() =>
|
||||
handleMenuNext(values.componentKind === WizardComponentKindEnum.Custom)
|
||||
}
|
||||
translate={translate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 2: Data Settings ───────────────────────────── */}
|
||||
{resolvedStep === 1 && (
|
||||
{/* ─── Step 4: Data Settings ───────────────────────────── */}
|
||||
{resolvedStep === STEP.settings && (
|
||||
<WizardStep2
|
||||
values={values}
|
||||
errors={errors}
|
||||
|
|
@ -1003,8 +1159,8 @@ const Wizard = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 3: List Form Fields ───────────────────────────── */}
|
||||
{resolvedStep === 2 && (
|
||||
{/* ─── Step 5: List Form Fields ───────────────────────────── */}
|
||||
{resolvedStep === STEP.fields && (
|
||||
<WizardStep3
|
||||
selectedColumns={selectedColumns}
|
||||
selectCommandColumns={selectCommandColumns}
|
||||
|
|
@ -1013,36 +1169,36 @@ const Wizard = () => {
|
|||
dbObjects={dbObjects}
|
||||
dsCode={currentDataSource}
|
||||
translate={translate}
|
||||
onBack={() => setCurrentStep(1)}
|
||||
onNext={() => setCurrentStep(3)}
|
||||
onBack={() => setCurrentStep(STEP.settings)}
|
||||
onNext={() => setCurrentStep(STEP.subForms)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 4: Sub Forms ───────────────────────────── */}
|
||||
{resolvedStep === 3 && (
|
||||
{/* ─── Step 6: Sub Forms ───────────────────────────── */}
|
||||
{resolvedStep === STEP.subForms && (
|
||||
<WizardStep4
|
||||
subForms={subForms}
|
||||
selectCommandColumns={selectCommandColumns}
|
||||
translate={translate}
|
||||
onChange={setSubForms}
|
||||
onBack={() => setCurrentStep(2)}
|
||||
onNext={() => setCurrentStep(4)}
|
||||
onBack={() => setCurrentStep(STEP.fields)}
|
||||
onNext={() => setCurrentStep(STEP.widgets)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 5: Widgets ───────────────────────────── */}
|
||||
{resolvedStep === 4 && (
|
||||
{/* ─── Step 7: Widgets ───────────────────────────── */}
|
||||
{resolvedStep === STEP.widgets && (
|
||||
<WizardStep5
|
||||
widgets={widgets}
|
||||
translate={translate}
|
||||
onChange={setWidgets}
|
||||
onBack={() => setCurrentStep(3)}
|
||||
onNext={() => setCurrentStep(5)}
|
||||
onBack={() => setCurrentStep(STEP.subForms)}
|
||||
onNext={() => setCurrentStep(STEP.workflow)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 6: Workflow ───────────────────────────── */}
|
||||
{resolvedStep === 5 && (
|
||||
{/* ─── Step 8: Workflow ───────────────────────────── */}
|
||||
{resolvedStep === STEP.workflow && (
|
||||
<WizardStep6
|
||||
listFormCode={values.listFormCode}
|
||||
workflow={workflow}
|
||||
|
|
@ -1051,13 +1207,13 @@ const Wizard = () => {
|
|||
translate={translate}
|
||||
onWorkflowChange={setWorkflow}
|
||||
onCriteriaChange={setCriteria}
|
||||
onBack={() => setCurrentStep(4)}
|
||||
onBack={() => setCurrentStep(STEP.widgets)}
|
||||
onNext={() => goToAdjacentStep(1)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 7: Todo / Kanban ────────────────────── */}
|
||||
{resolvedStep === 6 && values.todo && (
|
||||
{/* ─── Step 9: Todo / Kanban ────────────────────── */}
|
||||
{resolvedStep === STEP.todo && values.todo && (
|
||||
<WizardStep7
|
||||
selectedColumns={selectedColumns}
|
||||
selectCommandColumns={selectCommandColumns}
|
||||
|
|
@ -1067,8 +1223,8 @@ const Wizard = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 8: Tree ─────────────────────────────── */}
|
||||
{resolvedStep === 7 && values.tree && (
|
||||
{/* ─── Step 10: Tree ─────────────────────────────── */}
|
||||
{resolvedStep === STEP.tree && values.tree && (
|
||||
<WizardStep8
|
||||
selectCommandColumns={selectCommandColumns}
|
||||
translate={translate}
|
||||
|
|
@ -1077,8 +1233,8 @@ const Wizard = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 9: Gantt ────────────────────────────── */}
|
||||
{resolvedStep === 8 && values.gantt && (
|
||||
{/* ─── Step 11: Gantt ────────────────────────────── */}
|
||||
{resolvedStep === STEP.gantt && values.gantt && (
|
||||
<WizardStep9
|
||||
selectCommandColumns={selectCommandColumns}
|
||||
translate={translate}
|
||||
|
|
@ -1087,8 +1243,8 @@ const Wizard = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 10: Scheduler ───────────────────────── */}
|
||||
{resolvedStep === 9 && values.scheduler && (
|
||||
{/* ─── Step 12: Scheduler ───────────────────────── */}
|
||||
{resolvedStep === STEP.scheduler && values.scheduler && (
|
||||
<WizardStep10
|
||||
selectCommandColumns={selectCommandColumns}
|
||||
translate={translate}
|
||||
|
|
@ -1097,8 +1253,8 @@ const Wizard = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 11: Deploy ──────────────────────────── */}
|
||||
{resolvedStep === 10 && (
|
||||
{/* ─── Step 13: Deploy ──────────────────────────── */}
|
||||
{resolvedStep === STEP.deploy && (
|
||||
<WizardStep11
|
||||
values={values}
|
||||
wizardName={values.wizardName}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import classNames from 'classnames'
|
||||
import { Badge, Button, Dialog, Input, Notification, toast } from '@/components/ui'
|
||||
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
|
||||
import Container from '@/components/shared/Container'
|
||||
import {
|
||||
FaTrash,
|
||||
|
|
@ -12,6 +12,10 @@ import {
|
|||
FaSearch,
|
||||
FaEdit,
|
||||
FaCalendarAlt,
|
||||
FaCube,
|
||||
FaListUl,
|
||||
FaLink,
|
||||
FaExternalLinkAlt,
|
||||
} from 'react-icons/fa'
|
||||
import DbMigrateButton from '@/components/shared/DbMigrateButton'
|
||||
import { deleteWizardFile, getWizardFiles } from '@/services/wizard.service'
|
||||
|
|
@ -19,7 +23,7 @@ import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
|||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { WizardFileInfoDto } from '@/proxy/admin/wizard/models'
|
||||
import { WizardComponentKindEnum, WizardFileInfoDto } from '@/proxy/admin/wizard/models'
|
||||
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
|
||||
|
||||
interface ConfirmState {
|
||||
|
|
@ -141,7 +145,9 @@ const WizardFileManager = () => {
|
|||
className="flex items-center"
|
||||
>
|
||||
<FaPlus className="mr-1" />
|
||||
<span className="hidden sm:inline">{translate('::App.ListFormWizard.AddNewRecord')}</span>
|
||||
<span className="hidden sm:inline">
|
||||
{translate('::App.ListFormWizard.AddNewRecord')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -165,6 +171,7 @@ const WizardFileManager = () => {
|
|||
<div className="space-y-2">
|
||||
{filteredFiles.map((f) => {
|
||||
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
|
||||
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
|
||||
return (
|
||||
<div
|
||||
key={f.fileName}
|
||||
|
|
@ -172,15 +179,53 @@ const WizardFileManager = () => {
|
|||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<Icon className="text-indigo-400 shrink-0 text-3xl" />
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 space-y-1">
|
||||
{/* 1. satır: wizard adı */}
|
||||
<div className="font-medium text-sm text-gray-800 dark:text-gray-200 truncate">
|
||||
{f.wizardName || f.fileName} <Badge content={f.defaultLayout} />
|
||||
{f.wizardName || f.fileName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 flex gap-3 mt-0.5">
|
||||
|
||||
{/* 2. satır: dosya adı ve üretilen kod */}
|
||||
<div className="flex flex-wrap items-center gap-x-2 text-xs text-gray-400">
|
||||
<span className="truncate font-mono">{f.fileName}</span>
|
||||
<span className="text-gray-300 dark:text-gray-600">•</span>
|
||||
<span className="truncate">
|
||||
{f.fileName} - {f.listFormCode}
|
||||
{isCustom ? f.customComponentName : f.listFormCode}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 3. satır: yol ayrımı (Custom / List), layout ve menü adresi */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 text-xs">
|
||||
<span
|
||||
className={classNames(
|
||||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium',
|
||||
isCustom
|
||||
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
|
||||
)}
|
||||
>
|
||||
{isCustom ? <FaCube /> : <FaListUl />}
|
||||
{translate(
|
||||
isCustom
|
||||
? '::App.WizardStepComponent.CustomComponent'
|
||||
: '::App.WizardStepComponent.ListComponent',
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* Custom yolunda layout kavramı yoktur. */}
|
||||
{!isCustom && f.defaultLayout && (
|
||||
<span className="inline-flex items-center rounded-full bg-gray-200 px-2 py-0.5 font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
{f.defaultLayout}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{f.menuUrl && (
|
||||
<span className="inline-flex min-w-0 items-center gap-1 rounded-full bg-gray-200 px-2 py-0.5 font-mono text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
<FaLink className="shrink-0" />
|
||||
<span className="truncate">{f.menuUrl}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -193,6 +238,19 @@ const WizardFileManager = () => {
|
|||
<FaExclamationTriangle />
|
||||
</span>
|
||||
)}
|
||||
{/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */}
|
||||
{f.menuUrl && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="plain"
|
||||
className="text-emerald-500 hover:bg-emerald-50 dark:hover:bg-emerald-900/20"
|
||||
type="button"
|
||||
title={translate('::App.Platform.OpenUrl')}
|
||||
onClick={() => window.open(f.menuUrl, '_blank', 'noopener,noreferrer')}
|
||||
>
|
||||
<FaExternalLinkAlt />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="plain"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Button, FormItem, Input, Notification, Select, toast } from '@/components/ui'
|
||||
import { MenuDto } from '@/proxy/menus/models'
|
||||
import { SelectBoxOption } from '@/types/shared'
|
||||
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
|
||||
import { MenuItem } from '@/proxy/menus/menu'
|
||||
|
|
@ -8,18 +7,18 @@ import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import CreatableSelect from 'react-select/creatable'
|
||||
import {
|
||||
FaArrowLeft,
|
||||
FaArrowRight,
|
||||
FaChevronDown,
|
||||
FaChevronRight,
|
||||
FaEdit,
|
||||
FaPlus,
|
||||
FaTimes,
|
||||
FaCheck,
|
||||
FaTrash,
|
||||
} from 'react-icons/fa'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { IconPickerField, MenuAddDialog } from '@/views/shared/MenuAddDialog'
|
||||
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
|
||||
import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models'
|
||||
|
||||
// ─── Types (exported for Wizard.tsx) ─────────────────────────────────────────
|
||||
|
||||
|
|
@ -102,13 +101,8 @@ interface TreeNodeProps {
|
|||
onSelect: (code: string) => void
|
||||
expanded: Set<string>
|
||||
onToggle: (code: string) => void
|
||||
editingCode: string | null
|
||||
editingValue: string
|
||||
saving: boolean
|
||||
onStartEdit: (code: string, currentName: string) => void
|
||||
onEditChange: (v: string) => void
|
||||
onSaveEdit: (node: MenuTreeNode & { id?: string }) => void
|
||||
onCancelEdit: () => void
|
||||
onStartEdit: (node: MenuTreeNode & { id?: string }) => void
|
||||
onDelete: (node: MenuTreeNode & { id?: string }) => void
|
||||
}
|
||||
|
||||
|
|
@ -119,19 +113,13 @@ function TreeNode({
|
|||
onSelect,
|
||||
expanded,
|
||||
onToggle,
|
||||
editingCode,
|
||||
editingValue,
|
||||
saving,
|
||||
onStartEdit,
|
||||
onEditChange,
|
||||
onSaveEdit,
|
||||
onCancelEdit,
|
||||
onDelete,
|
||||
}: TreeNodeProps) {
|
||||
const hasChildren = node.children.length > 0
|
||||
const isExpanded = expanded.has(node.code)
|
||||
const isSelected = node.code === selectedCode
|
||||
const isEditing = editingCode === node.code
|
||||
const navigationIcon = useNavigationIcons()
|
||||
const NodeIcon = node.icon ? navigationIcon[node.icon] : null
|
||||
const { translate } = useLocalization()
|
||||
|
|
@ -168,92 +156,50 @@ function TreeNode({
|
|||
/>
|
||||
)}
|
||||
|
||||
{isEditing ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={editingValue}
|
||||
onChange={(e) => onEditChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onSaveEdit(node)
|
||||
if (e.key === 'Escape') onCancelEdit()
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex-1 px-2 py-0.5 text-sm rounded border border-indigo-400 outline-none bg-white text-gray-800 dark:bg-gray-700 dark:text-gray-100"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="flex-1 text-sm truncate cursor-pointer"
|
||||
onClick={() => onSelect(node.code)}
|
||||
>
|
||||
{node.displayName}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className="flex-1 text-sm truncate cursor-pointer"
|
||||
onClick={() => onSelect(node.code)}
|
||||
>
|
||||
{node.displayName}
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={`text-xs shrink-0 ${isSelected ? 'text-indigo-200' : 'text-gray-400'}`}
|
||||
onClick={() => !isEditing && onSelect(node.code)}
|
||||
onClick={() => onSelect(node.code)}
|
||||
>
|
||||
{translate('::' + node.displayName)} ({node.order})
|
||||
</span>
|
||||
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onSaveEdit(node)
|
||||
}}
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaCheck className="text-xs" />}
|
||||
className="!h-6 !w-6 !px-0 text-green-500 hover:!bg-transparent hover:text-green-600 disabled:opacity-40"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onCancelEdit()
|
||||
}}
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes className="text-xs" />}
|
||||
className="!h-6 !w-6 !px-0 text-gray-400 hover:!bg-transparent hover:text-gray-600"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<span className="shrink-0 flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
type="button"
|
||||
title={translate('::App.Listform.ListformField.Rename')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onStartEdit(node.code, node.displayName)
|
||||
}}
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaEdit className="text-xs" />}
|
||||
className={`!h-6 !w-6 !px-0 hover:!bg-transparent ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-indigo-500'}`}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
title={translate('::App.Platform.Delete')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete(node)
|
||||
}}
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTrash className="text-xs" />}
|
||||
className={`!h-6 !w-6 !px-0 hover:!bg-transparent ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-red-500'}`}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className="shrink-0 flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
title={translate('::App.Platform.EditMenu')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onStartEdit(node)
|
||||
}}
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaEdit className="text-xs" />}
|
||||
className={`!h-6 !w-6 !px-0 hover:!bg-transparent ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-indigo-500'}`}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
title={translate('::App.Platform.Delete')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete(node)
|
||||
}}
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTrash className="text-xs" />}
|
||||
className={`!h-6 !w-6 !px-0 hover:!bg-transparent ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-red-500'}`}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isExpanded &&
|
||||
|
|
@ -266,13 +212,8 @@ function TreeNode({
|
|||
onSelect={onSelect}
|
||||
expanded={expanded}
|
||||
onToggle={onToggle}
|
||||
editingCode={editingCode}
|
||||
editingValue={editingValue}
|
||||
saving={saving}
|
||||
onStartEdit={onStartEdit}
|
||||
onEditChange={onEditChange}
|
||||
onSaveEdit={onSaveEdit}
|
||||
onCancelEdit={onCancelEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -304,6 +245,8 @@ interface MenuTreeInlineProps {
|
|||
isLoading: boolean
|
||||
invalid?: boolean
|
||||
onReload: () => void
|
||||
/** Düzenleme dialogunu ilgili menü ile açar. */
|
||||
onEditMenu: (node: MenuTreeNode & { id?: string }) => void
|
||||
initialExpanded?: Set<string>
|
||||
}
|
||||
|
||||
|
|
@ -315,6 +258,7 @@ function MenuTreeInline({
|
|||
isLoading,
|
||||
invalid,
|
||||
onReload,
|
||||
onEditMenu,
|
||||
initialExpanded,
|
||||
}: MenuTreeInlineProps) {
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => initialExpanded ?? new Set())
|
||||
|
|
@ -330,8 +274,6 @@ function MenuTreeInline({
|
|||
return next
|
||||
})
|
||||
}, [value, rawItems])
|
||||
const [editingCode, setEditingCode] = useState<string | null>(null)
|
||||
const [editingValue, setEditingValue] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const { translate } = useLocalization()
|
||||
|
||||
|
|
@ -343,38 +285,10 @@ function MenuTreeInline({
|
|||
return n
|
||||
})
|
||||
|
||||
const handleStartEdit = (code: string, currentName: string) => {
|
||||
setEditingCode(code)
|
||||
setEditingValue(currentName)
|
||||
}
|
||||
|
||||
const handleSaveEdit = async (node: MenuTreeNode & { id?: string }) => {
|
||||
if (!editingValue.trim() || !node.id) return
|
||||
setSaving(true)
|
||||
try {
|
||||
const original = rawItems.find((i) => i.code === node.code)
|
||||
if (!original) return
|
||||
await menuService.update(node.id, {
|
||||
...original,
|
||||
displayName: editingValue.trim(),
|
||||
} as MenuDto)
|
||||
onReload()
|
||||
setEditingCode(null)
|
||||
} catch (e: any) {
|
||||
toast.push(<Notification title={e.message} type="danger" />, {
|
||||
placement: 'bottom-end',
|
||||
})
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (node: MenuTreeNode & { id?: string }) => {
|
||||
if (!node.id) return
|
||||
if (
|
||||
!window.confirm(
|
||||
`"${node.displayName}" ${translate('::App.WizardStep1.DeleteMenuConfirm')}`,
|
||||
)
|
||||
!window.confirm(`"${node.displayName}" ${translate('::App.WizardStep1.DeleteMenuConfirm')}`)
|
||||
)
|
||||
return
|
||||
setSaving(true)
|
||||
|
|
@ -404,21 +318,17 @@ function MenuTreeInline({
|
|||
const sharedNodeProps = {
|
||||
expanded,
|
||||
onToggle: toggle,
|
||||
editingCode,
|
||||
editingValue,
|
||||
saving,
|
||||
onStartEdit: handleStartEdit,
|
||||
onEditChange: setEditingValue,
|
||||
onSaveEdit: handleSaveEdit,
|
||||
onCancelEdit: () => setEditingCode(null),
|
||||
onStartEdit: onEditMenu,
|
||||
onDelete: handleDelete,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-lg border ${invalid ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'} bg-white dark:bg-gray-800 overflow-hidden`}
|
||||
className={`flex h-full min-h-[24rem] flex-col rounded-lg border ${invalid ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'} bg-white dark:bg-gray-800 overflow-hidden`}
|
||||
>
|
||||
<div className="h-96 overflow-y-auto py-1">
|
||||
{/* Yükseklik sağdaki alan kolonuna göre esner; taşma bu alanda kaydırılır. */}
|
||||
<div className="min-h-0 flex-1 overflow-y-auto py-1">
|
||||
{isLoading ? (
|
||||
<div className="px-4 py-3 text-sm text-gray-400">Loading…</div>
|
||||
) : enrichedNodes.length === 0 ? (
|
||||
|
|
@ -466,6 +376,7 @@ export interface WizardStep1Props {
|
|||
onReloadMenu: () => void
|
||||
permissionGroupList: SelectBoxOption[]
|
||||
isLoadingPermissionGroup: boolean
|
||||
onBack: () => void
|
||||
onNext: () => void
|
||||
translate: (key: string) => string
|
||||
}
|
||||
|
|
@ -485,11 +396,27 @@ const WizardStep1 = ({
|
|||
onReloadMenu,
|
||||
permissionGroupList,
|
||||
isLoadingPermissionGroup,
|
||||
onBack,
|
||||
onNext,
|
||||
translate,
|
||||
}: WizardStep1Props) => {
|
||||
const [menuDialogOpen, setMenuDialogOpen] = useState(false)
|
||||
const [menuDialogParentCode, setMenuDialogParentCode] = useState('')
|
||||
// Dolu ise dialog düzenleme modunda açılır.
|
||||
const [menuBeingEdited, setMenuBeingEdited] = useState<(MenuItem & { id?: string }) | null>(null)
|
||||
|
||||
/** Ağaç düğümünü ham menü kaydıyla eşleyip düzenleme dialogunu açar. */
|
||||
const handleEditMenu = (node: MenuTreeNode & { id?: string }) => {
|
||||
const original = rawMenuItems.find((item) => item.code === node.code)
|
||||
if (!original) return
|
||||
setMenuBeingEdited(original)
|
||||
setMenuDialogOpen(true)
|
||||
}
|
||||
|
||||
const closeMenuDialog = () => {
|
||||
setMenuDialogOpen(false)
|
||||
setMenuBeingEdited(null)
|
||||
}
|
||||
|
||||
const menuDialogInitialOrder = useMemo(() => {
|
||||
const maxOrder = rawMenuItems.reduce((max, item) => {
|
||||
|
|
@ -500,6 +427,17 @@ const WizardStep1 = ({
|
|||
return maxOrder + 100
|
||||
}, [rawMenuItems])
|
||||
|
||||
/**
|
||||
* Deploy'da menü kaydına yazılacak adres (backend: WizardConsts.MenuUrl / RoutePath).
|
||||
* Custom yolunda bileşenin kendi rotası, List yolunda üretilecek ListForm adresi.
|
||||
*/
|
||||
const isCustomComponent = values.componentKind === WizardComponentKindEnum.Custom
|
||||
const menuUrl = isCustomComponent
|
||||
? values.menuUrl
|
||||
: values.menuCode
|
||||
? `/admin/list/${values.menuCode}`
|
||||
: ''
|
||||
|
||||
const step1Missing = [
|
||||
!wizardName && translate('::App.WizardStep1.WizardName'),
|
||||
!values.menuCode && translate('::App.WizardStep1.MenuCode'),
|
||||
|
|
@ -538,9 +476,11 @@ const WizardStep1 = ({
|
|||
{/* Menu Code / Menu Text / Permission Group — 2-column grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
||||
{/* Col 1 */}
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
{/* Menu Parent */}
|
||||
<FormItem
|
||||
/* FormItem'in içerik kutusu da esnesin ki ağaç sağ kolon yüksekliğini doldursun. */
|
||||
className="flex flex-1 flex-col [&>div]:flex [&>div]:min-h-0 [&>div]:flex-1 [&>div]:flex-col"
|
||||
label={translate('::App.Platform.MenuParent')}
|
||||
invalid={false}
|
||||
errorMessage={undefined}
|
||||
|
|
@ -549,6 +489,7 @@ const WizardStep1 = ({
|
|||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMenuBeingEdited(null)
|
||||
setMenuDialogParentCode(
|
||||
values.menuParentCode
|
||||
? findRootCode(rawMenuItems, values.menuParentCode)
|
||||
|
|
@ -591,6 +532,7 @@ const WizardStep1 = ({
|
|||
isLoading={isLoadingMenu}
|
||||
invalid={false}
|
||||
onReload={onReloadMenu}
|
||||
onEditMenu={handleEditMenu}
|
||||
initialExpanded={
|
||||
values.menuParentCode
|
||||
? getAncestorCodes(rawMenuItems, values.menuParentCode)
|
||||
|
|
@ -603,11 +545,19 @@ const WizardStep1 = ({
|
|||
|
||||
<MenuAddDialog
|
||||
isOpen={menuDialogOpen}
|
||||
onClose={() => setMenuDialogOpen(false)}
|
||||
onClose={closeMenuDialog}
|
||||
initialParentCode={menuDialogParentCode}
|
||||
initialOrder={menuDialogInitialOrder}
|
||||
rawItems={rawMenuItems}
|
||||
onSaved={onMenuCreated}
|
||||
editMenu={menuBeingEdited}
|
||||
onSaved={async (menu) => {
|
||||
// Düzenlemede seçili parent'ı değiştirmemek için yalnızca ağaç tazelenir.
|
||||
if (menuBeingEdited) {
|
||||
await onReloadMenu()
|
||||
return
|
||||
}
|
||||
await onMenuCreated(menu)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -739,13 +689,34 @@ const WizardStep1 = ({
|
|||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
{/* Deploy sonrası menü ikonunun açacağı adres; seçilen yola göre değişir. */}
|
||||
<FormItem
|
||||
label={translate('::App.WizardStepComponent.MenuUrl')}
|
||||
extra={
|
||||
<span className="ml-2 text-xs text-gray-400">
|
||||
{translate(
|
||||
isCustomComponent
|
||||
? '::App.WizardStepComponent.MenuUrlFromComponent'
|
||||
: '::App.WizardStepComponent.MenuUrlFromList',
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="w-full rounded-md border border-gray-300 bg-gray-50 px-3 py-2 font-mono text-sm text-indigo-600 dark:border-gray-600 dark:bg-gray-800 dark:text-indigo-300">
|
||||
{menuUrl || '—'}
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Fixed Footer ─────────────────────────────── */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-0 h-16 flex items-center">
|
||||
<div className="flex items-center gap-3 w-full">
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<Button variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
||||
{translate('::App.Platform.Back')}
|
||||
</Button>
|
||||
<div className="flex-1 flex items-center justify-end gap-2">
|
||||
{!step1CanGo && (
|
||||
<span className="text-xs text-amber-600 dark:text-amber-400 font-medium">
|
||||
⚠ {translate('::App.ListFormWizard.Required')}: {step1Missing.join(', ')}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from 'react-icons/fa'
|
||||
import { WizardGroup } from './WizardStep3'
|
||||
import { dbSourceTypeOptions, selectCommandTypeOptions } from '../edit/options'
|
||||
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
|
||||
import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models'
|
||||
import {
|
||||
ListFormWorkflowCriteriaDto,
|
||||
SubFormDto,
|
||||
|
|
@ -69,6 +69,39 @@ function buildLogSteps(
|
|||
translate: (key: string, params?: Record<string, string | number>) => string,
|
||||
): Omit<LogEntry, 'status'>[] {
|
||||
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
|
||||
|
||||
// Custom yolunda veri kaynağı / ListForm / alan üretimi hiç çalışmaz.
|
||||
if (values.componentKind === WizardComponentKindEnum.Custom) {
|
||||
return [
|
||||
{ id: 1, label: translate('::App.Step4Log.ValidatingConfig') },
|
||||
{
|
||||
id: 2,
|
||||
label: translate('::App.Step4Log.CreatingMenu', { 0: values.menuCode }),
|
||||
detail: `Parent: ${values.menuParentCode}`,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: translate('::App.Step4Log.SavingLanguageTexts'),
|
||||
detail: `EN: ${values.languageTextMenuEn} / TR: ${values.languageTextMenuTr}`,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: translate('::App.Step4Log.ConfiguringPermission', {
|
||||
0: values.permissionGroupName,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: translate('::App.Step4Log.LinkingCustomComponent', {
|
||||
0: values.customComponentName,
|
||||
}),
|
||||
detail: values.menuUrl,
|
||||
},
|
||||
{ id: 8, label: translate('::App.Step4Log.Deploying') },
|
||||
{ id: 9, label: translate('::App.Step4Log.Completed') },
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{ id: 1, label: translate('::App.Step4Log.ValidatingConfig') },
|
||||
{
|
||||
|
|
@ -259,6 +292,13 @@ const WizardStep11 = ({
|
|||
)
|
||||
const workflowItems = criteria.length > 0 ? criteria : (workflow.criteria ?? [])
|
||||
|
||||
/**
|
||||
* Custom Component yolunda ListForm üretilmediği için layout, alanlar, subform,
|
||||
* widget ve workflow özetlerinin hiçbiri anlamlı değildir; yalnızca menü ve
|
||||
* bağlanan bileşen gösterilir.
|
||||
*/
|
||||
const isCustom = values.componentKind === WizardComponentKindEnum.Custom
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[3fr_2fr] gap-5 pb-24 items-start">
|
||||
{/* ── Left: Summary ──────────────────────────────────────────── */}
|
||||
|
|
@ -267,23 +307,14 @@ const WizardStep11 = ({
|
|||
<Section title={translate('::App.WizardStep4.MenuInfo')}>
|
||||
<Row label={translate('::App.WizardStep4.WizardName')} value={wizardName} />
|
||||
<Row label={translate('::App.WizardStep1.MenuCode')} value={values.menuCode} />
|
||||
<Row
|
||||
label={translate('::App.Platform.MenuParent')}
|
||||
value={values.menuParentCode}
|
||||
/>
|
||||
<Row label={translate('::App.Platform.MenuParent')} value={values.menuParentCode} />
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.PermissionGroup')}
|
||||
value={values.permissionGroupName}
|
||||
/>
|
||||
<Row label={translate('::App.Listform.ListformField.Icon')} value={values.menuIcon} />
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.MenuTr')}
|
||||
value={values.languageTextMenuTr}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.MenuEn')}
|
||||
value={values.languageTextMenuEn}
|
||||
/>
|
||||
<Row label={translate('::App.WizardStep4.MenuTr')} value={values.languageTextMenuTr} />
|
||||
<Row label={translate('::App.WizardStep4.MenuEn')} value={values.languageTextMenuEn} />
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.MenuParentTr')}
|
||||
value={values.languageTextMenuParentTr}
|
||||
|
|
@ -294,188 +325,202 @@ const WizardStep11 = ({
|
|||
/>
|
||||
</Section>
|
||||
|
||||
<Section title={translate('::App.WizardStep4.ListFormSettings')}>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.ListFormCode')}
|
||||
value={values.listFormCode}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.TitleTr')}
|
||||
value={values.languageTextTitleTr}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.TitleEn')}
|
||||
value={values.languageTextTitleEn}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.DescTr')}
|
||||
value={values.languageTextDescTr}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.DescEn')}
|
||||
value={values.languageTextDescEn}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.Platform.DataSource')}
|
||||
value={values.dataSourceCode}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.Listform.ListformField.ConnectionString')}
|
||||
value={maskConnectionString(values.dataSourceConnectionString)}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep2.SelectCommand')}
|
||||
value={`${values.selectCommand} (${
|
||||
selectCommandTypeOptions.find((o: any) => o.value === values.selectCommandType)
|
||||
?.label ?? String(values.selectCommandType)
|
||||
})`}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.KeyField')}
|
||||
value={`${values.keyFieldName} (${
|
||||
dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType)
|
||||
?.label ?? String(values.keyFieldDbSourceType)
|
||||
})`}
|
||||
/>
|
||||
</Section>
|
||||
{isCustom ? (
|
||||
<Section title={translate('::App.WizardStepComponent.CustomComponent')}>
|
||||
<Row
|
||||
label={translate('::App.WizardStepComponent.ComponentName')}
|
||||
value={values.customComponentName}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStepComponent.RoutePath')}
|
||||
value={values.menuUrl}
|
||||
/>
|
||||
</Section>
|
||||
) : (
|
||||
<Section title={translate('::App.WizardStep4.ListFormSettings')}>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.ListFormCode')}
|
||||
value={values.listFormCode}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.TitleTr')}
|
||||
value={values.languageTextTitleTr}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.TitleEn')}
|
||||
value={values.languageTextTitleEn}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.DescTr')}
|
||||
value={values.languageTextDescTr}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.DescEn')}
|
||||
value={values.languageTextDescEn}
|
||||
/>
|
||||
<Row label={translate('::App.Platform.DataSource')} value={values.dataSourceCode} />
|
||||
<Row
|
||||
label={translate('::App.Listform.ListformField.ConnectionString')}
|
||||
value={maskConnectionString(values.dataSourceConnectionString)}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep2.SelectCommand')}
|
||||
value={`${values.selectCommand} (${
|
||||
selectCommandTypeOptions.find((o: any) => o.value === values.selectCommandType)
|
||||
?.label ?? String(values.selectCommandType)
|
||||
})`}
|
||||
/>
|
||||
<Row
|
||||
label={translate('::App.WizardStep4.KeyField')}
|
||||
value={`${values.keyFieldName} (${
|
||||
dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType)
|
||||
?.label ?? String(values.keyFieldDbSourceType)
|
||||
})`}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Section
|
||||
title={translate('::App.WizardStep4.ColumnsAndFormLayout')}
|
||||
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::App.WizardStep4.EditingForm')}`}
|
||||
>
|
||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{
|
||||
label: translate('::App.WizardStep4.SelectedColumns'),
|
||||
value: selectedColumns.size,
|
||||
className: 'text-indigo-600 dark:text-indigo-400',
|
||||
},
|
||||
{
|
||||
label: translate('::App.WizardStep4.EditingForm'),
|
||||
value: editingFormFields.length,
|
||||
className: 'text-emerald-600 dark:text-emerald-400',
|
||||
},
|
||||
{
|
||||
label: translate('::App.WizardStep4.FormGroups'),
|
||||
value: groups.length,
|
||||
className: 'text-gray-700 dark:text-gray-200',
|
||||
},
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="rounded-lg border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 px-3 py-2"
|
||||
>
|
||||
<div className={`text-lg font-semibold leading-none ${item.className}`}>
|
||||
{item.value}
|
||||
{!isCustom && (
|
||||
<Section
|
||||
title={translate('::App.WizardStep4.ColumnsAndFormLayout')}
|
||||
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::App.WizardStep4.EditingForm')}`}
|
||||
>
|
||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{
|
||||
label: translate('::App.WizardStep4.SelectedColumns'),
|
||||
value: selectedColumns.size,
|
||||
className: 'text-indigo-600 dark:text-indigo-400',
|
||||
},
|
||||
{
|
||||
label: translate('::App.WizardStep4.EditingForm'),
|
||||
value: editingFormFields.length,
|
||||
className: 'text-emerald-600 dark:text-emerald-400',
|
||||
},
|
||||
{
|
||||
label: translate('::App.WizardStep4.FormGroups'),
|
||||
value: groups.length,
|
||||
className: 'text-gray-700 dark:text-gray-200',
|
||||
},
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="rounded-lg border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 px-3 py-2"
|
||||
>
|
||||
<div className={`text-lg font-semibold leading-none ${item.className}`}>
|
||||
{item.value}
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-gray-400 truncate">{item.label}</div>
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-gray-400 truncate">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{groups.map((g) => (
|
||||
<div
|
||||
key={g.id}
|
||||
className="overflow-hidden rounded-lg border border-gray-100 dark:border-gray-800"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 bg-gray-50 dark:bg-gray-800 px-3 py-2">
|
||||
<span className="text-xs font-semibold text-gray-700 dark:text-gray-200 truncate">
|
||||
{g.caption || `(${translate('::App.Listform.ListformField.Group')})`}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-gray-400 shrink-0">
|
||||
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
|
||||
{g.items.length} {translate('::App.Platform.Field')}
|
||||
</span>
|
||||
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
|
||||
{g.colCount} {translate('::App.Listform.ListformField.Column')}
|
||||
<div className="flex flex-col gap-2">
|
||||
{groups.map((g) => (
|
||||
<div
|
||||
key={g.id}
|
||||
className="overflow-hidden rounded-lg border border-gray-100 dark:border-gray-800"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 bg-gray-50 dark:bg-gray-800 px-3 py-2">
|
||||
<span className="text-xs font-semibold text-gray-700 dark:text-gray-200 truncate">
|
||||
{g.caption || `(${translate('::App.Listform.ListformField.Group')})`}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-gray-400 shrink-0">
|
||||
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
|
||||
{g.items.length} {translate('::App.Platform.Field')}
|
||||
</span>
|
||||
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
|
||||
{g.colCount} {translate('::App.Listform.ListformField.Column')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{g.items.length === 0 ? (
|
||||
<span className="block px-3 py-2 text-xs text-gray-300 italic">
|
||||
{translate('::App.WizardStep4.NoFields')}
|
||||
</span>
|
||||
) : (
|
||||
g.items.map((item) => {
|
||||
const meta = selectCommandColumns.find(
|
||||
(c) => c.columnName === item.fieldName,
|
||||
)
|
||||
const isKeyField = item.fieldName === values.keyFieldName
|
||||
const isPopupField = item.includeInEditingForm && !isKeyField
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="grid grid-cols-[minmax(150px,1fr)_auto] items-center gap-3 px-3 py-1.5"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 truncate">
|
||||
{item.fieldName}
|
||||
</span>
|
||||
{meta?.dataType && (
|
||||
<span className="text-[10px] text-gray-400 truncate">
|
||||
{meta.dataType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-end gap-1">
|
||||
<span className="rounded bg-indigo-50 dark:bg-indigo-900/30 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:text-indigo-300">
|
||||
{translate('::App.WizardStep4.SelectedColumns')}
|
||||
</span>
|
||||
{isPopupField && (
|
||||
<span className="rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300">
|
||||
{translate('::App.WizardStep4.EditingForm')}
|
||||
</span>
|
||||
)}
|
||||
{isKeyField && (
|
||||
<span className="rounded bg-amber-50 dark:bg-amber-900/30 px-1.5 py-0.5 text-[10px] text-amber-600 dark:text-amber-300">
|
||||
{translate('::App.WizardStep4.KeyField')}
|
||||
</span>
|
||||
)}
|
||||
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
|
||||
{item.editorType}
|
||||
</span>
|
||||
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
|
||||
span-{item.colSpan}
|
||||
{item.isRequired && <span className="ml-1 text-red-400">*</span>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{g.items.length === 0 ? (
|
||||
<span className="block px-3 py-2 text-xs text-gray-300 italic">
|
||||
{translate('::App.WizardStep4.NoFields')}
|
||||
</span>
|
||||
) : (
|
||||
g.items.map((item) => {
|
||||
const meta = selectCommandColumns.find((c) => c.columnName === item.fieldName)
|
||||
const isKeyField = item.fieldName === values.keyFieldName
|
||||
const isPopupField = item.includeInEditingForm && !isKeyField
|
||||
))}
|
||||
{ungroupedSelectedColumns.length > 0 && (
|
||||
<div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2">
|
||||
<div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300">
|
||||
{translate('::App.WizardStep4.UngroupedColumns')}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{ungroupedSelectedColumns.map((col) => {
|
||||
const meta = selectCommandColumns.find((c) => c.columnName === col)
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="grid grid-cols-[minmax(150px,1fr)_auto] items-center gap-3 px-3 py-1.5"
|
||||
<span
|
||||
key={col}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-amber-200 dark:border-amber-800 bg-white dark:bg-gray-900 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-300"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 truncate">
|
||||
{item.fieldName}
|
||||
{col}
|
||||
{meta?.dataType && (
|
||||
<span className="text-[10px] text-amber-500 opacity-70">
|
||||
{meta.dataType}
|
||||
</span>
|
||||
{meta?.dataType && (
|
||||
<span className="text-[10px] text-gray-400 truncate">
|
||||
{meta.dataType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-end gap-1">
|
||||
<span className="rounded bg-indigo-50 dark:bg-indigo-900/30 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:text-indigo-300">
|
||||
{translate('::App.WizardStep4.SelectedColumns')}
|
||||
</span>
|
||||
{isPopupField && (
|
||||
<span className="rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300">
|
||||
{translate('::App.WizardStep4.EditingForm')}
|
||||
</span>
|
||||
)}
|
||||
{isKeyField && (
|
||||
<span className="rounded bg-amber-50 dark:bg-amber-900/30 px-1.5 py-0.5 text-[10px] text-amber-600 dark:text-amber-300">
|
||||
{translate('::App.WizardStep4.KeyField')}
|
||||
</span>
|
||||
)}
|
||||
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
|
||||
{item.editorType}
|
||||
</span>
|
||||
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
|
||||
span-{item.colSpan}
|
||||
{item.isRequired && <span className="ml-1 text-red-400">*</span>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
)}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{ungroupedSelectedColumns.length > 0 && (
|
||||
<div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2">
|
||||
<div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300">
|
||||
{translate('::App.WizardStep4.UngroupedColumns')}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{ungroupedSelectedColumns.map((col) => {
|
||||
const meta = selectCommandColumns.find((c) => c.columnName === col)
|
||||
return (
|
||||
<span
|
||||
key={col}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-amber-200 dark:border-amber-800 bg-white dark:bg-gray-900 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-300"
|
||||
>
|
||||
{col}
|
||||
{meta?.dataType && (
|
||||
<span className="text-[10px] text-amber-500 opacity-70">
|
||||
{meta.dataType}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{subForms.length > 0 && (
|
||||
{!isCustom && subForms.length > 0 && (
|
||||
<Section title={translate('::App.ListFormEdit.SubForms')} badge={subForms.length}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{subForms.map((subForm, index) => (
|
||||
|
|
@ -508,7 +553,7 @@ const WizardStep11 = ({
|
|||
</Section>
|
||||
)}
|
||||
|
||||
{widgets.length > 0 && (
|
||||
{!isCustom && widgets.length > 0 && (
|
||||
<Section title={translate('::App.ListFormEdit.TabWidgets')} badge={widgets.length}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{widgets.map((widget, index) => (
|
||||
|
|
@ -532,11 +577,8 @@ const WizardStep11 = ({
|
|||
</Section>
|
||||
)}
|
||||
|
||||
{(hasWorkflowFields || workflowItems.length > 0) && (
|
||||
<Section
|
||||
title={translate('::App.ListFormEdit.TabWorkflow')}
|
||||
badge={workflowItems.length}
|
||||
>
|
||||
{!isCustom && (hasWorkflowFields || workflowItems.length > 0) && (
|
||||
<Section title={translate('::App.ListFormEdit.TabWorkflow')} badge={workflowItems.length}>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Row label="Approval User" value={workflow.approvalUserFieldName} />
|
||||
|
|
@ -582,7 +624,7 @@ const WizardStep11 = ({
|
|||
</Section>
|
||||
)}
|
||||
|
||||
{values.todo && (
|
||||
{!isCustom && values.todo && (
|
||||
<Section title={translate('::App.Platform.TabTodo')}>
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<Row
|
||||
|
|
@ -640,47 +682,49 @@ const WizardStep11 = ({
|
|||
|
||||
{/* ── Right: Deploy ──────────────────────────────────────────── */}
|
||||
<div className="sticky top-4 flex flex-col gap-3 max-h-[calc(100vh-200px)]">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-8 gap-2">
|
||||
{[
|
||||
{ label: translate('::App.Listform.ListformField.Group'), value: groups.length },
|
||||
{ label: translate('::App.Platform.Field'), value: totalFields },
|
||||
{
|
||||
label: translate('::App.WizardStep4.EditingForm'),
|
||||
value: editingFormFields.length,
|
||||
},
|
||||
{
|
||||
label: translate('::App.Listform.ListformField.Column'),
|
||||
value: selectedColumns.size,
|
||||
},
|
||||
{
|
||||
label: translate('::App.ListFormEdit.SubForms'),
|
||||
value: subForms.length,
|
||||
},
|
||||
{
|
||||
label: translate('::App.ListFormEdit.TabWidgets'),
|
||||
value: widgets.length,
|
||||
},
|
||||
{
|
||||
label: translate('::App.ListFormEdit.TabWorkflow'),
|
||||
value: workflowItems.length,
|
||||
},
|
||||
{
|
||||
label: translate('::App.Platform.TabTodo'),
|
||||
value: values.todo ? 1 : 0,
|
||||
},
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3 text-center shadow-sm"
|
||||
>
|
||||
<div className="text-2xl font-bold text-indigo-600 dark:text-indigo-400">
|
||||
{s.value}
|
||||
{/* Stats — Custom yolunda sayilacak ListForm artefakti yok. */}
|
||||
{!isCustom && (
|
||||
<div className="grid grid-cols-8 gap-2">
|
||||
{[
|
||||
{ label: translate('::App.Listform.ListformField.Group'), value: groups.length },
|
||||
{ label: translate('::App.Platform.Field'), value: totalFields },
|
||||
{
|
||||
label: translate('::App.WizardStep4.EditingForm'),
|
||||
value: editingFormFields.length,
|
||||
},
|
||||
{
|
||||
label: translate('::App.Listform.ListformField.Column'),
|
||||
value: selectedColumns.size,
|
||||
},
|
||||
{
|
||||
label: translate('::App.ListFormEdit.SubForms'),
|
||||
value: subForms.length,
|
||||
},
|
||||
{
|
||||
label: translate('::App.ListFormEdit.TabWidgets'),
|
||||
value: widgets.length,
|
||||
},
|
||||
{
|
||||
label: translate('::App.ListFormEdit.TabWorkflow'),
|
||||
value: workflowItems.length,
|
||||
},
|
||||
{
|
||||
label: translate('::App.Platform.TabTodo'),
|
||||
value: values.todo ? 1 : 0,
|
||||
},
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3 text-center shadow-sm"
|
||||
>
|
||||
<div className="text-2xl font-bold text-indigo-600 dark:text-indigo-400">
|
||||
{s.value}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">{s.label}</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Log panel — grows to fill remaining height */}
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col flex-1 min-h-0">
|
||||
|
|
|
|||
|
|
@ -501,7 +501,8 @@ const WizardStep2 = ({
|
|||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-x-6">
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{/* Menü URL'i Menu adımında gösterilir; burada yalnızca layout seçimi kalır. */}
|
||||
<div className="w-32">
|
||||
<FormItem
|
||||
label={translate('::App.ListFormEditDetailsLayoutDto.DefaultLayout')}
|
||||
invalid={errors?.defaultLayout && touched?.defaultLayout}
|
||||
|
|
|
|||
118
ui/src/views/admin/listForm/wizard/WizardStepComponent.tsx
Normal file
118
ui/src/views/admin/listForm/wizard/WizardStepComponent.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { Button } from '@/components/ui'
|
||||
import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models'
|
||||
import { FaArrowRight, FaCheck, FaCube, FaTable } from 'react-icons/fa'
|
||||
|
||||
// ─── Props ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface WizardStepComponentProps {
|
||||
values: ListFormWizardDto
|
||||
onKindChange: (kind: WizardComponentKindEnum) => void
|
||||
translate: (key: string) => string
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
// ─── Kind card ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface KindCardProps {
|
||||
active: boolean
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
description: string
|
||||
onSelect: () => void
|
||||
}
|
||||
|
||||
function KindCard({ active, icon, title, description, onSelect }: KindCardProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`group relative flex flex-col items-center gap-3 rounded-2xl border-2 px-6 py-8 text-center transition-all ${
|
||||
active
|
||||
? 'border-indigo-500 bg-indigo-50 shadow-md dark:border-indigo-500 dark:bg-indigo-900/20'
|
||||
: 'border-gray-200 bg-white hover:-translate-y-0.5 hover:border-indigo-300 hover:shadow-sm dark:border-gray-700 dark:bg-gray-800 dark:hover:border-indigo-600'
|
||||
}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
{active && (
|
||||
<span className="absolute right-3 top-3 flex h-5 w-5 items-center justify-center rounded-full bg-indigo-500 text-[10px] text-white">
|
||||
<FaCheck />
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`flex h-14 w-14 items-center justify-center rounded-full text-2xl transition-colors ${
|
||||
active
|
||||
? 'bg-indigo-500 text-white'
|
||||
: 'bg-gray-100 text-gray-400 group-hover:bg-indigo-100 group-hover:text-indigo-500 dark:bg-gray-700 dark:group-hover:bg-indigo-900/40'
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span className="text-base font-semibold text-gray-800 dark:text-gray-100">{title}</span>
|
||||
<span className="text-xs leading-relaxed text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── WizardStepComponent ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Wizard'ın yol ayrımı adımı: yalnızca tip seçimi yapılır.
|
||||
* List seçilirse mevcut ListForm akışı aynen devam eder; Custom seçilirse bir
|
||||
* sonraki adımda bileşen seçimi yapılır ve ListForm'a ait tüm adımlar devre dışı kalır.
|
||||
*/
|
||||
const WizardStepComponent = ({
|
||||
values,
|
||||
onKindChange,
|
||||
translate,
|
||||
onNext,
|
||||
}: WizardStepComponentProps) => {
|
||||
const isCustom = values.componentKind === WizardComponentKindEnum.Custom
|
||||
|
||||
return (
|
||||
<div className="pb-20">
|
||||
{/* Bu adımda tek bir karar var; seçim ekranın ortasına yerleşir. */}
|
||||
<div className="flex min-h-[calc(100vh-320px)] items-center justify-center">
|
||||
<div className="w-full max-w-3xl">
|
||||
<div className="mb-8 text-center">
|
||||
<h4 className="text-lg font-semibold text-gray-800 dark:text-gray-100">
|
||||
{translate('::App.WizardStepComponent.ComponentType')}
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{translate('::App.WizardStepComponent.ComponentTypeHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<KindCard
|
||||
active={!isCustom}
|
||||
icon={<FaTable />}
|
||||
title={translate('::App.WizardStepComponent.ListComponent')}
|
||||
description={translate('::App.WizardStepComponent.ListComponentHint')}
|
||||
onSelect={() => onKindChange(WizardComponentKindEnum.List)}
|
||||
/>
|
||||
<KindCard
|
||||
active={isCustom}
|
||||
icon={<FaCube />}
|
||||
title={translate('::App.WizardStepComponent.CustomComponent')}
|
||||
description={translate('::App.WizardStepComponent.CustomComponentHint')}
|
||||
onSelect={() => onKindChange(WizardComponentKindEnum.Custom)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Fixed Footer ─────────────────────────────── */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 py-0 dark:border-gray-700 dark:bg-gray-900">
|
||||
{/* Wizard'in ilk adimi oldugu icin geri butonu yok. */}
|
||||
<div className="flex w-full items-center justify-end gap-3">
|
||||
<Button variant="solid" type="button" icon={<FaArrowRight />} onClick={onNext}>
|
||||
{translate('::App.Platform.Next')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WizardStepComponent
|
||||
196
ui/src/views/admin/listForm/wizard/WizardStepCustomComponent.tsx
Normal file
196
ui/src/views/admin/listForm/wizard/WizardStepCustomComponent.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import { Button, FormItem, Select } from '@/components/ui'
|
||||
import { useComponents } from '@/contexts/ComponentContext'
|
||||
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
|
||||
import { Field, FieldProps } from 'formik'
|
||||
import { useMemo } from 'react'
|
||||
import { FaArrowLeft, FaArrowRight, FaCube, FaExternalLinkAlt } from 'react-icons/fa'
|
||||
|
||||
// ─── Props ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface WizardStepCustomComponentProps {
|
||||
values: ListFormWizardDto
|
||||
onCustomComponentChange: (name: string, routePath: string) => void
|
||||
translate: (key: string) => string
|
||||
onBack: () => void
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
// ─── WizardStepCustomComponent ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Custom yolunda menüye bağlanacak bileşenin seçildiği adım. Seçimle birlikte
|
||||
* bileşenin rota yolu ve canlı önizlemesi gösterilir; deploy sonrası menü ikonu
|
||||
* tam olarak bu rotayı açar.
|
||||
*/
|
||||
const WizardStepCustomComponent = ({
|
||||
values,
|
||||
onCustomComponentChange,
|
||||
translate,
|
||||
onBack,
|
||||
onNext,
|
||||
}: WizardStepCustomComponentProps) => {
|
||||
const { components, compiling, componentErrors, renderComponent } = useComponents()
|
||||
|
||||
// Yalnızca aktif ve rota tanımlı bileşenler menüye bağlanabilir; pasif bir
|
||||
// bileşenin rotası router'a hiç kaydedilmediği için menü boşa düşerdi.
|
||||
const selectableComponents = useMemo(
|
||||
() => components.filter((c) => c.isActive && c.routePath?.trim()),
|
||||
[components],
|
||||
)
|
||||
|
||||
const componentOptions = useMemo(
|
||||
() =>
|
||||
selectableComponents.map((c) => ({
|
||||
value: c.name,
|
||||
label: c.description?.trim() ? `${c.name} — ${c.description.trim()}` : c.name,
|
||||
})),
|
||||
[selectableComponents],
|
||||
)
|
||||
|
||||
const selectedComponent = useMemo(
|
||||
() => selectableComponents.find((c) => c.name === values.customComponentName),
|
||||
[selectableComponents, values.customComponentName],
|
||||
)
|
||||
|
||||
const compileError = values.customComponentName
|
||||
? componentErrors[values.customComponentName]
|
||||
: undefined
|
||||
|
||||
const stepMissing = [
|
||||
!values.customComponentName && translate('::App.WizardStepComponent.ComponentName'),
|
||||
!!values.customComponentName &&
|
||||
!values.menuUrl &&
|
||||
translate('::App.WizardStepComponent.RoutePath'),
|
||||
].filter(Boolean) as string[]
|
||||
const canGo = stepMissing.length === 0
|
||||
|
||||
return (
|
||||
<div className="pb-20">
|
||||
<div className="grid grid-cols-1 gap-x-6 md:grid-cols-2">
|
||||
<FormItem label={translate('::App.WizardStepComponent.ComponentName')} asterisk={true}>
|
||||
<Field type="text" autoComplete="off" name="customComponentName">
|
||||
{({ field, form }: FieldProps<string>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable={true}
|
||||
options={componentOptions}
|
||||
placeholder={translate('::App.WizardStepComponent.SelectComponent')}
|
||||
value={
|
||||
values.customComponentName
|
||||
? (componentOptions.find((o) => o.value === values.customComponentName) ?? {
|
||||
label: values.customComponentName,
|
||||
value: values.customComponentName,
|
||||
})
|
||||
: null
|
||||
}
|
||||
onChange={(option) => {
|
||||
const name = option?.value ?? ''
|
||||
const routePath =
|
||||
selectableComponents.find((c) => c.name === name)?.routePath ?? ''
|
||||
onCustomComponentChange(name, routePath)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
{/* Deploy sonrası menüye eklenen ikon bu rotayı açar. */}
|
||||
<FormItem
|
||||
label={translate('::App.WizardStepComponent.RoutePath')}
|
||||
extra={
|
||||
<span className="ml-2 text-xs text-gray-400">
|
||||
{translate('::App.WizardStepComponent.RoutePathHint')}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="min-w-0 flex-1 rounded-md border border-gray-300 bg-gray-50 px-3 py-2 font-mono text-sm text-indigo-600 dark:border-gray-600 dark:bg-gray-800 dark:text-indigo-300">
|
||||
{values.menuUrl || '—'}
|
||||
</div>
|
||||
{values.menuUrl && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="default"
|
||||
icon={<FaExternalLinkAlt />}
|
||||
title={translate('::App.Platform.OpenUrl')}
|
||||
onClick={() => window.open(values.menuUrl, '_blank')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{selectableComponents.length === 0 && (
|
||||
<div className="rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-700 dark:border-amber-700 dark:bg-amber-900/20 dark:text-amber-300">
|
||||
{translate('::App.WizardStepComponent.NoComponents')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Önizleme ───────────────────────────────────────────────── */}
|
||||
{values.customComponentName ? (
|
||||
<FormItem
|
||||
label={translate('::App.WizardStepComponent.Preview')}
|
||||
extra={
|
||||
selectedComponent?.description?.trim() ? (
|
||||
<span className="ml-2 text-xs text-gray-400">
|
||||
{selectedComponent.description.trim()}
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<div className="min-h-[320px] overflow-auto rounded-lg border border-gray-300 bg-white p-4 dark:border-gray-600 dark:bg-gray-900">
|
||||
{compiling ? (
|
||||
<div className="py-6 text-center text-sm text-gray-400">
|
||||
{translate('::App.Platform.LoadingWithThreeDot')}
|
||||
</div>
|
||||
) : compileError ? (
|
||||
<div className="whitespace-pre-wrap rounded border-2 border-red-300 bg-red-50 p-3 text-sm text-red-700 dark:border-red-700 dark:bg-red-900/20 dark:text-red-300">
|
||||
{compileError}
|
||||
</div>
|
||||
) : (
|
||||
renderComponent(values.customComponentName)
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
) : (
|
||||
selectableComponents.length > 0 && (
|
||||
<div className="flex min-h-[320px] flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed border-gray-200 text-gray-300 dark:border-gray-700 dark:text-gray-600">
|
||||
<FaCube className="text-3xl" />
|
||||
<span className="select-none text-sm">
|
||||
{translate('::App.WizardStepComponent.SelectComponent')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* ─── Fixed Footer ─────────────────────────────── */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 py-0 dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<Button variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
||||
{translate('::App.Platform.Back')}
|
||||
</Button>
|
||||
<div className="flex flex-1 items-center justify-end gap-3">
|
||||
{!canGo && (
|
||||
<span className="text-xs font-medium text-amber-600 dark:text-amber-400">
|
||||
⚠ {translate('::App.ListFormWizard.Required')}: {stepMissing.join(', ')}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="solid"
|
||||
type="button"
|
||||
icon={<FaArrowRight />}
|
||||
disabled={!canGo}
|
||||
onClick={onNext}
|
||||
>
|
||||
{translate('::App.Platform.Next')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WizardStepCustomComponent
|
||||
|
|
@ -4,7 +4,7 @@ import { MenuItem } from '@/proxy/menus/menu'
|
|||
import { MenuService } from '@/services/menu.service'
|
||||
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FaChevronDown, FaPlus } from 'react-icons/fa'
|
||||
import { FaChevronDown, FaEdit, FaPlus } from 'react-icons/fa'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { useConfig } from '@/components/ui/ConfigProvider'
|
||||
import { useForm } from '@/components/ui/Form/context'
|
||||
|
|
@ -83,9 +83,7 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
|||
<span className="truncate">{value}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex-1 !text-gray-400">
|
||||
{translate('::App.WizardStep1.SelectIcon')}
|
||||
</span>
|
||||
<span className="flex-1 !text-gray-400">{translate('::App.WizardStep1.SelectIcon')}</span>
|
||||
)}
|
||||
<FaChevronDown
|
||||
className={`shrink-0 text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
|
|
@ -153,6 +151,11 @@ export interface MenuAddDialogProps {
|
|||
initialParentCode: string
|
||||
initialOrder: number
|
||||
rawItems: (MenuItem & { id?: string })[]
|
||||
/**
|
||||
* Dolu ise dialog düzenleme modunda açılır ve menünün tüm alanları değiştirilebilir.
|
||||
* Boş bırakılırsa yeni menü oluşturma modudur.
|
||||
*/
|
||||
editMenu?: (MenuItem & { id?: string }) | null
|
||||
onSaved: (menu: {
|
||||
code: string
|
||||
parentCode?: string
|
||||
|
|
@ -163,42 +166,86 @@ export interface MenuAddDialogProps {
|
|||
}) => void | Promise<void>
|
||||
}
|
||||
|
||||
/** Düzenleme modunda menü kendi alt ağacına taşınamaz; aksi halde döngü oluşur. */
|
||||
function collectDescendantCodes(
|
||||
items: (MenuItem & { id?: string })[],
|
||||
rootCode: string,
|
||||
): Set<string> {
|
||||
const result = new Set<string>([rootCode])
|
||||
let grew = true
|
||||
while (grew) {
|
||||
grew = false
|
||||
for (const item of items) {
|
||||
if (!item.code || result.has(item.code)) continue
|
||||
if (item.parentCode && result.has(item.parentCode)) {
|
||||
result.add(item.code)
|
||||
grew = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const emptyForm = (parentCode: string, order: number) => ({
|
||||
name: '',
|
||||
code: '',
|
||||
menuTextEn: '',
|
||||
menuTextTr: '',
|
||||
parentCode,
|
||||
icon: '',
|
||||
shortName: '',
|
||||
order,
|
||||
url: '',
|
||||
isDisabled: false,
|
||||
})
|
||||
|
||||
export function MenuAddDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
initialParentCode,
|
||||
initialOrder,
|
||||
rawItems,
|
||||
editMenu,
|
||||
onSaved,
|
||||
}: MenuAddDialogProps) {
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
code: '',
|
||||
menuTextEn: '',
|
||||
menuTextTr: '',
|
||||
parentCode: initialParentCode,
|
||||
icon: '',
|
||||
shortName: '',
|
||||
order: initialOrder,
|
||||
})
|
||||
const isEditMode = !!editMenu?.id
|
||||
const [form, setForm] = useState(() => emptyForm(initialParentCode, initialOrder))
|
||||
const [saving, setSaving] = useState(false)
|
||||
const { translate } = useLocalization()
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen)
|
||||
if (!isOpen) return
|
||||
if (editMenu?.id) {
|
||||
setForm({
|
||||
name: '',
|
||||
code: '',
|
||||
menuTextEn: '',
|
||||
menuTextTr: '',
|
||||
parentCode: initialParentCode,
|
||||
icon: '',
|
||||
shortName: '',
|
||||
order: initialOrder,
|
||||
name: editMenu.code ?? '',
|
||||
code: editMenu.code ?? '',
|
||||
menuTextEn: editMenu.menuTextEn ?? '',
|
||||
menuTextTr: editMenu.menuTextTr ?? '',
|
||||
parentCode: editMenu.parentCode ?? '',
|
||||
icon: editMenu.icon ?? '',
|
||||
shortName: editMenu.shortName ?? '',
|
||||
order: editMenu.order ?? 0,
|
||||
url: editMenu.url ?? '',
|
||||
isDisabled: editMenu.isDisabled ?? false,
|
||||
})
|
||||
}, [isOpen, initialParentCode, initialOrder])
|
||||
return
|
||||
}
|
||||
setForm(emptyForm(initialParentCode, initialOrder))
|
||||
}, [isOpen, initialParentCode, initialOrder, editMenu])
|
||||
|
||||
// Kök menülerde kısa ad zorunlu; alt menülerde isteğe bağlıdır.
|
||||
const shortNameRequired = !form.parentCode.trim()
|
||||
|
||||
// Düzenlenen menü, kendisi veya alt ağacındaki bir menünün altına taşınamaz.
|
||||
const parentOptions = useMemo(() => {
|
||||
if (!isEditMode) return []
|
||||
const blocked = collectDescendantCodes(rawItems, editMenu?.code ?? '')
|
||||
return rawItems
|
||||
.filter((item) => item.code && !blocked.has(item.code))
|
||||
.map((item) => ({ code: item.code!, label: item.code! }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
}, [isEditMode, rawItems, editMenu])
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.code.trim() || !form.menuTextEn.trim()) return
|
||||
if (shortNameRequired && !form.shortName.trim()) return
|
||||
|
|
@ -206,17 +253,27 @@ export function MenuAddDialog({
|
|||
try {
|
||||
const savedMenu = {
|
||||
code: form.code.trim(),
|
||||
// Görünen ad dil anahtarıdır; menü kodu ile aynı kalır.
|
||||
displayName: form.code.trim(),
|
||||
parentCode: form.parentCode.trim() || undefined,
|
||||
icon: form.icon || undefined,
|
||||
shortName: form.shortName.trim() || undefined,
|
||||
order: form.order,
|
||||
isDisabled: false,
|
||||
isDisabled: form.isDisabled,
|
||||
url: form.url.trim() || undefined,
|
||||
menuTextTr: form.menuTextTr.trim(),
|
||||
menuTextEn: form.menuTextEn.trim(),
|
||||
} as MenuDto
|
||||
|
||||
await menuService.createWithLanguageKeyText(savedMenu)
|
||||
if (isEditMode) {
|
||||
// Kod ve görünen ad dışındaki tüm alanlar güncellenir; sunucu kodu korur.
|
||||
await menuService.updateWithLanguageKeyText(editMenu!.id!, {
|
||||
...(editMenu as MenuDto),
|
||||
...savedMenu,
|
||||
})
|
||||
} else {
|
||||
await menuService.createWithLanguageKeyText(savedMenu)
|
||||
}
|
||||
|
||||
await onSaved({
|
||||
code: savedMenu.code!,
|
||||
|
|
@ -236,9 +293,6 @@ export function MenuAddDialog({
|
|||
}
|
||||
}
|
||||
|
||||
// suppress unused warning — rawItems kept for future use
|
||||
void rawItems
|
||||
|
||||
const fieldCls =
|
||||
'h-9 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 outline-none focus:border-indigo-400 w-full'
|
||||
const disabledCls =
|
||||
|
|
@ -257,9 +311,15 @@ export function MenuAddDialog({
|
|||
<div className="flex flex-col gap-5 p-1">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 pb-1 border-b border-gray-100 dark:border-gray-700">
|
||||
<FaPlus className="text-green-500 text-sm" />
|
||||
{isEditMode ? (
|
||||
<FaEdit className="text-indigo-500 text-sm" />
|
||||
) : (
|
||||
<FaPlus className="text-green-500 text-sm" />
|
||||
)}
|
||||
<h5 className="text-base font-semibold text-gray-800 dark:text-gray-100">
|
||||
{translate('::App.Platform.AddNewMenu')}
|
||||
{isEditMode
|
||||
? translate('::App.Platform.EditMenu')
|
||||
: translate('::App.Platform.AddNewMenu')}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
|
|
@ -270,8 +330,10 @@ export function MenuAddDialog({
|
|||
{translate('::App.Listform.ListformField.Name')}{' '}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
{/* Ad, kodu ve dil anahtarını türetir; düzenlemede kod sabit olduğu için kilitlidir. */}
|
||||
<input
|
||||
autoFocus
|
||||
autoFocus={!isEditMode}
|
||||
disabled={isEditMode}
|
||||
value={form.name}
|
||||
onChange={(e) => {
|
||||
const codeToken = toCodeToken(e.target.value)
|
||||
|
|
@ -287,7 +349,7 @@ export function MenuAddDialog({
|
|||
}))
|
||||
}}
|
||||
placeholder="MyMenu"
|
||||
className={fieldCls}
|
||||
className={isEditMode ? disabledCls : fieldCls}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
|
|
@ -346,7 +408,22 @@ export function MenuAddDialog({
|
|||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col">
|
||||
<label className={labelCls}>{translate('::App.Platform.MenuParent')}</label>
|
||||
<input disabled value={form.parentCode} className={disabledCls} />
|
||||
{isEditMode ? (
|
||||
<select
|
||||
value={form.parentCode}
|
||||
onChange={(e) => setForm((p) => ({ ...p, parentCode: e.target.value }))}
|
||||
className={fieldCls}
|
||||
>
|
||||
<option value="">{translate('::App.WizardStep1.RootMenu')}</option>
|
||||
{parentOptions.map((option) => (
|
||||
<option key={option.code} value={option.code}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input disabled value={form.parentCode} className={disabledCls} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label className={labelCls}>{translate('::App.Listform.ListformField.Order')}</label>
|
||||
|
|
@ -378,12 +455,43 @@ export function MenuAddDialog({
|
|||
className={`${fieldCls}`}
|
||||
/>
|
||||
{shortNameRequired && (
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{translate('::App.Platform.MenuCodeHint')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">{translate('::App.Platform.MenuCodeHint')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 6 — Url | Durum (yalnızca düzenlemede) */}
|
||||
{isEditMode && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col">
|
||||
<label className={labelCls}>{translate('::App.Platform.OpenUrl')}</label>
|
||||
<input
|
||||
value={form.url}
|
||||
onChange={(e) => setForm((p) => ({ ...p, url: e.target.value }))}
|
||||
placeholder="/admin/list/App.Wizard.MyMenu"
|
||||
className={`${fieldCls} font-mono`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label className={labelCls}>{translate('::App.Platform.Status')}</label>
|
||||
<label className="flex h-9 cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!form.isDisabled}
|
||||
onChange={(e) => setForm((p) => ({ ...p, isDisabled: !e.target.checked }))}
|
||||
className="h-4 w-4 accent-indigo-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200">
|
||||
{translate(
|
||||
form.isDisabled
|
||||
? '::App.Platform.Passive'
|
||||
: '::App.Listform.ListformField.IsActive',
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-2 pt-1 border-t border-gray-100 dark:border-gray-700">
|
||||
<Button type="button" size="sm" variant="plain" onClick={onClose}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue