Menüsüz Wizard ekleme

This commit is contained in:
Sedat ÖZTÜRK 2026-08-24 15:00:31 +03:00
parent b1dac82d56
commit ec6b35d6d5
11 changed files with 517 additions and 391 deletions

View file

@ -27,6 +27,14 @@ public class ListFormWizardDto
public string MenuCode { get; set; } public string MenuCode { get; set; }
public int MenuOrder { get; set; } public int MenuOrder { get; set; }
/// <summary>
/// Wizard'in menuye eklenip eklenmeyecegi. False ise menu (ve gerekiyorsa ust menu) kaydi
/// hic uretilmez; ListForm, permission ve dil anahtarlari her iki durumda da uretilir.
/// Menusuz wizard'lar baska bir ekranda SubGrid/parca olarak kullanilir.
/// Varsayilan true oldugu icin bu alani tasimayan eski seed dosyalari eskisi gibi calisir.
/// </summary>
public bool CreateMenu { get; set; } = true;
/// <summary>Custom yolunda menuye baglanacak CustomComponent'in adi.</summary> /// <summary>Custom yolunda menuye baglanacak CustomComponent'in adi.</summary>
public string CustomComponentName { get; set; } public string CustomComponentName { get; set; }

View file

@ -133,7 +133,11 @@ public class ListFormWizardAppService(
} }
//Permission Group //Permission Group
var groupName = input.PermissionGroupName ?? PlatformConsts.AppName; // Menuye eklenmeyen wizard'da izin grubu sorulmaz; bos geldiginde varsayilan gruba dusar.
var groupName = string.IsNullOrWhiteSpace(input.PermissionGroupName)
? PlatformConsts.AppName
: input.PermissionGroupName.Trim();
input.PermissionGroupName = groupName;
if (!await repoPermGroup.AnyAsync(a => a.Name == groupName)) if (!await repoPermGroup.AnyAsync(a => a.Name == groupName))
{ {
await repoPermGroup.InsertAsync(new PermissionGroupDefinitionRecord(GuidGenerator.Create(), groupName, groupName), autoSave: false); await repoPermGroup.InsertAsync(new PermissionGroupDefinitionRecord(GuidGenerator.Create(), groupName, groupName), autoSave: false);
@ -208,69 +212,75 @@ public class ListFormWizardAppService(
await permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: false); await permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: false);
} }
//Menu Parent // Menunun acacagi adres: Custom yolunda CustomComponent'in kendi rotasi (bu rota
var menuQueryable = await repoMenu.GetQueryableAsync(); // mapCustomComponentRoutes tarafindan zaten kayitlidir), aksi halde ListForm adresi.
var menuParent = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == input.MenuParentCode)); // Seed dosyasindan da gorulebilmesi icin input'a geri yazilir; menu uretilmese de
if (menuParent == null) // ListForm'un adresi degismedigi icin ayni sekilde hesaplanir.
{ input.MenuUrl = isCustomComponent ? customRoutePath : WizardConsts.MenuUrl(code);
var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0;
var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon)
? input.MenuParentIcon
: !string.IsNullOrWhiteSpace(input.MenuIcon)
? input.MenuIcon
: WizardConsts.MenuIcon;
await CreateLangKey(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr, inserted);
menuParent = await repoMenu.InsertAsync(new Menu
{
Code = input.MenuParentCode,
DisplayName = input.MenuParentCode,
IsDisabled = false,
ShortName = input.MenuParentShortName?.Trim(),
Icon = menuParentIcon,
Order = maxRootOrder + 1,
}, autoSave: false);
inserted.MenuCodes.Add(input.MenuParentCode);
}
else if (string.IsNullOrWhiteSpace(menuParent.ShortName) &&
!string.IsNullOrWhiteSpace(input.MenuParentShortName))
{
menuParent.ShortName = input.MenuParentShortName.Trim();
await repoMenu.UpdateAsync(menuParent, autoSave: false);
}
//Menu // Menusuz wizard: menu (ve ust menu) kaydi hic uretilmez, geri kalan her sey aynidir.
var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0; if (input.CreateMenu)
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)
{ {
await repoMenu.InsertAsync(new Menu //Menu Parent
var menuQueryable = await repoMenu.GetQueryableAsync();
var menuParent = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == input.MenuParentCode));
if (menuParent == null)
{ {
Code = code, var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0;
DisplayName = nameLangKey, var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon)
IsDisabled = false, ? input.MenuParentIcon
ParentCode = menuParent.Code, : !string.IsNullOrWhiteSpace(input.MenuIcon)
Icon = input.MenuIcon ?? WizardConsts.MenuIcon, ? input.MenuIcon
Target = null, : WizardConsts.MenuIcon;
ElementId = null, await CreateLangKey(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr, inserted);
CssClass = null, menuParent = await repoMenu.InsertAsync(new Menu
Url = menuUrl, {
RequiredPermissionName = permRead.Name, Code = input.MenuParentCode,
Order = menuOrder, DisplayName = input.MenuParentCode,
}, autoSave: false); IsDisabled = false,
inserted.MenuCodes.Add(code); ShortName = input.MenuParentShortName?.Trim(),
} Icon = menuParentIcon,
else if (existingMenu.Order != menuOrder || existingMenu.Url != menuUrl) Order = maxRootOrder + 1,
{ }, autoSave: false);
existingMenu.Order = menuOrder; inserted.MenuCodes.Add(input.MenuParentCode);
existingMenu.Url = menuUrl; }
await repoMenu.UpdateAsync(existingMenu, autoSave: false); else if (string.IsNullOrWhiteSpace(menuParent.ShortName) &&
!string.IsNullOrWhiteSpace(input.MenuParentShortName))
{
menuParent.ShortName = input.MenuParentShortName.Trim();
await repoMenu.UpdateAsync(menuParent, autoSave: false);
}
//Menu
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;
var menuUrl = input.MenuUrl;
var existingMenu = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == code));
if (existingMenu == null)
{
await repoMenu.InsertAsync(new Menu
{
Code = code,
DisplayName = nameLangKey,
IsDisabled = false,
ParentCode = menuParent.Code,
Icon = input.MenuIcon ?? WizardConsts.MenuIcon,
Target = null,
ElementId = null,
CssClass = null,
Url = menuUrl,
RequiredPermissionName = permRead.Name,
Order = menuOrder,
}, autoSave: false);
inserted.MenuCodes.Add(code);
}
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, // Custom yolunda ListForm ekosistemi üretilmez: veri kaynağı, ListForm, alanlar,

View file

@ -1503,8 +1503,8 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Listforms.Listform", "key": "App.Listforms.Listform",
"en": "Listforms", "en": "ListForm Components",
"tr": "Listformlar" "tr": "List Form Bileşenleri"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
@ -16326,6 +16326,18 @@
"en": "Automatically assigned, editable", "en": "Automatically assigned, editable",
"tr": "Otomatik atanır, düzenlenebilir" "tr": "Otomatik atanır, düzenlenebilir"
}, },
{
"resourceName": "Platform",
"key": "App.WizardStep1.CreateMenu",
"en": "Show in menu",
"tr": "Menüde göster"
},
{
"resourceName": "Platform",
"key": "App.WizardStep1.CreateMenuHint",
"en": "If off, no menu record is created; the list is still usable as a sub grid or component",
"tr": "Kapalıysa menü kaydı oluşturulmaz; liste yine SubGrid veya bileşen olarak kullanılabilir"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Platform.Add", "key": "App.Platform.Add",
@ -16836,12 +16848,6 @@
"en": "Decides whether the wizard generates a list form or links an existing custom component", "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" "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", "resourceName": "Platform",
"key": "App.WizardStepComponent.ListComponentHint", "key": "App.WizardStepComponent.ListComponentHint",

View file

@ -797,16 +797,6 @@
"RequiredPermissionName": "App.DataSource", "RequiredPermissionName": "App.DataSource",
"IsDisabled": false "IsDisabled": false
}, },
{
"ParentCode": "App.Saas",
"Code": "App.Listforms.Listform",
"DisplayName": "App.Listforms.Listform",
"Order": 9,
"Url": "/admin/list/App.Listforms.Listform",
"Icon": "FcDataSheet",
"RequiredPermissionName": "App.Listforms.Listform",
"IsDisabled": false
},
{ {
"ParentCode": "App.Saas", "ParentCode": "App.Saas",
"Code": "App.Notifications", "Code": "App.Notifications",
@ -978,6 +968,16 @@
"RequiredPermissionName": "App.DeveloperKit.Components", "RequiredPermissionName": "App.DeveloperKit.Components",
"IsDisabled": false "IsDisabled": false
}, },
{
"ParentCode": "App.DeveloperKit",
"Code": "App.Listforms.Listform",
"DisplayName": "App.Listforms.Listform",
"Order": 6,
"Url": "/admin/list/App.Listforms.Listform",
"Icon": "FcDataSheet",
"RequiredPermissionName": "App.Listforms.Listform",
"IsDisabled": false
},
{ {
"ParentCode": "App.DeveloperKit", "ParentCode": "App.DeveloperKit",
"Code": "App.Listforms.WizardManager", "Code": "App.Listforms.WizardManager",

View file

@ -117,8 +117,12 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
// Zaten seeded mi kontrol et. Custom yolunda ListForm üretilmediği için // Zaten seeded mi kontrol et. Custom yolunda ListForm üretilmediği için
// idempotency menü kaydı üzerinden değerlendirilir. // idempotency menü kaydı üzerinden değerlendirilir.
// Menusuz Custom wizard'da ne ListForm ne de menu kaydi olustugu icin
// idempotency okuma permission'i uzerinden degerlendirilir.
var alreadySeeded = seedFile.Wizard.ComponentKind == WizardComponentKindEnum.Custom var alreadySeeded = seedFile.Wizard.ComponentKind == WizardComponentKindEnum.Custom
? await _repoMenu.AnyAsync(a => a.Code == ResolveMenuCode(seedFile.Wizard)) ? seedFile.Wizard.CreateMenu
? await _repoMenu.AnyAsync(a => a.Code == ResolveMenuCode(seedFile.Wizard))
: await _repoPerm.AnyAsync(a => a.Name == ResolveMenuCode(seedFile.Wizard))
: await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode); : await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode);
if (alreadySeeded) if (alreadySeeded)
@ -181,7 +185,9 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
} }
// Permission Group // Permission Group
var groupName = input.PermissionGroupName ?? AppName; // Menuye eklenmeyen wizard'da izin grubu sorulmaz; bos geldiginde varsayilan gruba dusar.
var groupName = string.IsNullOrWhiteSpace(input.PermissionGroupName) ? AppName : input.PermissionGroupName.Trim();
input.PermissionGroupName = groupName;
if (!await _repoPermGroup.AnyAsync(a => a.Name == groupName)) if (!await _repoPermGroup.AnyAsync(a => a.Name == groupName))
{ {
await _repoPermGroup.InsertAsync( await _repoPermGroup.InsertAsync(
@ -249,64 +255,68 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
// await _permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: true); // await _permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: true);
// } // }
// Menu Parent // Menunun acacagi adres; menu uretilmese de seed dosyasindaki degeri korunur.
var menuQueryable = await _repoMenu.GetQueryableAsync(); input.MenuUrl = isCustomComponent
var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode);
if (menuParent == null)
{
var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0;
var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon)
? input.MenuParentIcon
: !string.IsNullOrWhiteSpace(input.MenuIcon)
? input.MenuIcon
: WizardConsts.MenuIcon;
await CreateLangKeyAsync(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr);
menuParent = await _repoMenu.InsertAsync(new Menu
{
Code = input.MenuParentCode,
DisplayName = input.MenuParentCode,
IsDisabled = false,
ShortName = input.MenuParentShortName,
Icon = menuParentIcon,
Order = maxRootOrder + 1,
}, autoSave: true);
}
// Menu
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 ? customRoutePath
: !string.IsNullOrWhiteSpace(input.MenuUrl) : !string.IsNullOrWhiteSpace(input.MenuUrl)
? input.MenuUrl.Trim() ? input.MenuUrl.Trim()
: WizardConsts.MenuUrl(code); : WizardConsts.MenuUrl(code);
input.MenuUrl = menuUrl;
var existingMenu = await _repoMenu.FirstOrDefaultAsync(a => a.Code == code); // Menusuz wizard: menu (ve ust menu) kaydi hic uretilmez.
if (existingMenu == null) if (input.CreateMenu)
{ {
await _repoMenu.InsertAsync(new Menu // Menu Parent
var menuQueryable = await _repoMenu.GetQueryableAsync();
var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode);
if (menuParent == null)
{ {
Code = code, var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0;
DisplayName = nameLangKey, var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon)
IsDisabled = false, ? input.MenuParentIcon
ParentCode = menuParent.Code, : !string.IsNullOrWhiteSpace(input.MenuIcon)
Icon = input.MenuIcon ?? WizardConsts.MenuIcon, ? input.MenuIcon
Target = null, : WizardConsts.MenuIcon;
ElementId = null, await CreateLangKeyAsync(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr);
CssClass = null, menuParent = await _repoMenu.InsertAsync(new Menu
Url = menuUrl, {
RequiredPermissionName = permRead.Name, Code = input.MenuParentCode,
Order = menuOrder, DisplayName = input.MenuParentCode,
}, autoSave: true); IsDisabled = false,
} ShortName = input.MenuParentShortName,
else if (existingMenu.Order != menuOrder || existingMenu.Url != menuUrl) Icon = menuParentIcon,
{ Order = maxRootOrder + 1,
existingMenu.Order = menuOrder; }, autoSave: true);
existingMenu.Url = menuUrl; }
await _repoMenu.UpdateAsync(existingMenu, autoSave: true);
// Menu
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;
var menuUrl = input.MenuUrl;
var existingMenu = await _repoMenu.FirstOrDefaultAsync(a => a.Code == code);
if (existingMenu == null)
{
await _repoMenu.InsertAsync(new Menu
{
Code = code,
DisplayName = nameLangKey,
IsDisabled = false,
ParentCode = menuParent.Code,
Icon = input.MenuIcon ?? WizardConsts.MenuIcon,
Target = null,
ElementId = null,
CssClass = null,
Url = menuUrl,
RequiredPermissionName = permRead.Name,
Order = menuOrder,
}, autoSave: true);
}
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. // Custom yolunda ListForm ekosistemi üretilmez.

View file

@ -47,6 +47,12 @@ export interface ListFormWizardDto {
menuCode: string menuCode: string
menuOrder: number menuOrder: number
/**
* Wizard menüye eklensin mi. False ise menü kaydı hiç üretilmez; ListForm, permission ve
* dil anahtarları yine üretilir (SubGrid gibi başka bir ekranın parçası olarak kullanılır).
*/
createMenu: boolean
customComponentName: string customComponentName: string
/** Menünün açtığı adres; Custom yolunda seçilen bileşenin rota yolu. */ /** Menünün açtığı adres; Custom yolunda seçilen bileşenin rota yolu. */
menuUrl: string menuUrl: string

View file

@ -55,6 +55,7 @@ const initialValues: ListFormWizardDto = {
listFormCode: '', listFormCode: '',
menuCode: '', menuCode: '',
menuOrder: 0, menuOrder: 0,
createMenu: true,
componentKind: WizardComponentKindEnum.List, componentKind: WizardComponentKindEnum.List,
customComponentName: '', customComponentName: '',
menuUrl: '', menuUrl: '',
@ -174,7 +175,16 @@ const initialValues: ListFormWizardDto = {
const step1ValidationSchema = Yup.object().shape({ const step1ValidationSchema = Yup.object().shape({
wizardName: Yup.string().required(), wizardName: Yup.string().required(),
menuCode: Yup.string().required(), menuCode: Yup.string().required(),
menuOrder: Yup.number().integer().min(1).required(), // Menüye eklenmeyen wizard'da sıra alanı hiç sorulmaz; zorunluluğu da kalkar.
menuOrder: Yup.number()
.integer()
.when('createMenu', {
is: (createMenu: boolean) => createMenu !== false,
then: (schema) => schema.min(1).required(),
otherwise: (schema) => schema.optional(),
}),
createMenu: Yup.boolean(),
// İzin grubu menüden bağımsızdır: menüsüz wizard'da da izinler üretilir ve bir gruba yazılır.
permissionGroupName: Yup.string().required(), permissionGroupName: Yup.string().required(),
languageTextMenuEn: Yup.string().required(), languageTextMenuEn: Yup.string().required(),
languageTextMenuTr: Yup.string().required(), languageTextMenuTr: Yup.string().required(),
@ -595,6 +605,8 @@ const Wizard = () => {
wizardName: w.wizardName ?? '', wizardName: w.wizardName ?? '',
listFormCode: w.listFormCode ?? '', listFormCode: w.listFormCode ?? '',
menuCode: w.menuCode ?? '', menuCode: w.menuCode ?? '',
// Alanı taşımayan eski seed dosyaları menülü wizard olarak açılır.
createMenu: w.createMenu ?? true,
// Bu alanları içermeyen eski seed dosyaları List yolunda açılır. // Bu alanları içermeyen eski seed dosyaları List yolunda açılır.
componentKind: w.componentKind ?? WizardComponentKindEnum.List, componentKind: w.componentKind ?? WizardComponentKindEnum.List,
customComponentName: w.customComponentName ?? '', customComponentName: w.customComponentName ?? '',
@ -830,6 +842,23 @@ const Wizard = () => {
formikRef.current?.setFieldValue('permissionGroupName', rootCode) formikRef.current?.setFieldValue('permissionGroupName', rootCode)
} }
/**
* Menüye eklenmeyecek wizard'da menüye özel alanlar temizlenir; aksi halde seed dosyasına
* hiç kullanılmayacak üst menü/ikon/sıra değerleri yazılır ve deploy özeti yanıltır.
*/
const handleCreateMenuChange = (createMenu: boolean) => {
const formik = formikRef.current
formik?.setFieldValue('createMenu', createMenu)
if (createMenu) return
formik?.setFieldValue('menuParentCode', '')
formik?.setFieldValue('menuParentShortName', '')
formik?.setFieldValue('menuParentIcon', '')
formik?.setFieldValue('languageTextMenuParentEn', '')
formik?.setFieldValue('languageTextMenuParentTr', '')
formik?.setFieldValue('menuIcon', '')
formik?.setFieldValue('menuOrder', 0)
}
const handleMenuParentChange = (code: string) => { const handleMenuParentChange = (code: string) => {
const formik = formikRef.current const formik = formikRef.current
const parentChanged = formik?.values.menuParentCode !== code const parentChanged = formik?.values.menuParentCode !== code
@ -1180,6 +1209,7 @@ const Wizard = () => {
menuTree={menuTree} menuTree={menuTree}
isLoadingMenu={isLoadingMenu} isLoadingMenu={isLoadingMenu}
onMenuParentChange={handleMenuParentChange} onMenuParentChange={handleMenuParentChange}
onCreateMenuChange={handleCreateMenuChange}
onClearMenuParent={() => { onClearMenuParent={() => {
formikRef.current?.setFieldValue('menuParentCode', '') formikRef.current?.setFieldValue('menuParentCode', '')
formikRef.current?.setFieldValue('menuParentShortName', '') formikRef.current?.setFieldValue('menuParentShortName', '')

View file

@ -354,11 +354,11 @@ const WizardFileManager = () => {
className="flex flex-col gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800" className="flex flex-col gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
> >
<div className="flex items-start gap-2 min-w-0"> <div className="flex items-start gap-2 min-w-0">
<Icon className="text-indigo-400 shrink-0 text-2xl" /> <Icon className="text-indigo-400 shrink-0 text-lg mt-0.5" />
<div className="min-w-0 flex-1 space-y-1"> <div className="min-w-0 flex-1 space-y-1">
{/* 1. satır: wizard adı */} {/* 1. satır: wizard adı */}
<div <div
className="font-medium text-sm text-gray-800 dark:text-gray-200 truncate" className="font-medium text-sm leading-6 text-gray-800 dark:text-gray-200 truncate"
title={f.wizardName || f.fileName} title={f.wizardName || f.fileName}
> >
{f.wizardName || f.fileName} {f.wizardName || f.fileName}
@ -368,85 +368,9 @@ const WizardFileManager = () => {
<div className="text-xs text-gray-400 truncate font-mono" title={f.fileName}> <div className="text-xs text-gray-400 truncate font-mono" title={f.fileName}>
{f.fileName} {f.fileName}
</div> </div>
<div className="text-xs text-gray-400 truncate">
{isCustom ? f.customComponentName : f.listFormCode}
</div>
</div>
</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 max-w-full 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"
title={f.menuUrl}
>
<FaLink className="shrink-0" />
<span className="truncate">{f.menuUrl}</span>
</span>
)}
</div>
{renderActions(
f,
'flex items-center justify-end gap-1 pt-1 border-t border-gray-200 dark:border-gray-700',
)}
</div>
)
})}
</div>
) : (
<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}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
>
<div className="flex items-center gap-3 min-w-0">
<Icon className="text-indigo-400 shrink-0 text-3xl" />
<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}
</div>
{/* 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">
{isCustom ? f.customComponentName : f.listFormCode}
</span>
</div>
{/* 3. satır: yol ayrımı (Custom / List), layout ve menü adresi */} {/* 3. satır: yol ayrımı (Custom / List), layout ve menü adresi */}
<div className="flex flex-wrap items-center gap-1.5 text-xs"> <div className="flex flex-wrap pt-1 items-center gap-1.5 text-xs">
<span <span
className={classNames( className={classNames(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium', 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium',
@ -459,10 +383,15 @@ const WizardFileManager = () => {
{translate( {translate(
isCustom isCustom
? '::App.WizardStepComponent.CustomComponent' ? '::App.WizardStepComponent.CustomComponent'
: '::App.WizardStepComponent.ListComponent', : '::App.Listforms.Listform',
)} )}
</span> </span>
<div className="text-xs text-gray-400 truncate">
{isCustom ? f.customComponentName : f.listFormCode}
</div>
</div>
<div className="flex flex-wrap pt-1 items-center gap-1.5 text-xs">
{/* Custom yolunda layout kavramı yoktur. */} {/* Custom yolunda layout kavramı yoktur. */}
{!isCustom && f.defaultLayout && ( {!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"> <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">
@ -471,7 +400,10 @@ const WizardFileManager = () => {
)} )}
{f.menuUrl && ( {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"> <span
className="inline-flex min-w-0 max-w-full 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"
title={f.menuUrl}
>
<FaLink className="shrink-0" /> <FaLink className="shrink-0" />
<span className="truncate">{f.menuUrl}</span> <span className="truncate">{f.menuUrl}</span>
</span> </span>
@ -480,11 +412,103 @@ const WizardFileManager = () => {
</div> </div>
</div> </div>
{renderActions(f, 'flex items-center gap-1 shrink-0 sm:ml-3')} {renderActions(
f,
'flex items-center justify-end gap-1 pt-1 border-t border-gray-200 dark:border-gray-700',
)}
</div> </div>
) )
})} })}
</div> </div>
) : (
<div className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-600 dark:text-gray-300">
<tr>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Name')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.FileManager.FileName')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Code')}
</th>
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
{translate('::App.WizardStepComponent.ComponentType')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.WizardStepComponent.MenuUrl')}
</th>
<th className="text-right font-medium px-4 py-2">
{translate('::App.Platform.Actions')}
</th>
</tr>
</thead>
<tbody>
{filteredFiles.map((f) => {
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
return (
<tr
key={f.fileName}
className="border-t border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800"
>
<td className="px-4 py-2">
<div className="flex items-center gap-2 min-w-0">
<Icon className="text-indigo-400 shrink-0 text-lg" />
<span
className="font-medium text-gray-800 dark:text-gray-200 truncate"
title={f.wizardName || f.fileName}
>
{f.wizardName || f.fileName}
</span>
</div>
</td>
<td
className="px-4 py-2 font-mono text-gray-500 dark:text-gray-400"
title={f.fileName}
>
{f.fileName}
</td>
<td className="px-4 py-2 text-gray-600 dark:text-gray-300">
{isCustom ? f.customComponentName : f.listFormCode}
</td>
<td className="px-4 py-2 whitespace-nowrap">
<span
className={classNames(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs 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.Listforms.Listform',
)}
</span>
</td>
<td
className="px-4 py-2 font-mono text-gray-600 dark:text-gray-300"
title={f.menuUrl}
>
<div className="flex items-center gap-1 min-w-0">
{f.menuUrl && <FaLink className="shrink-0 text-gray-400" />}
<span className="truncate">{f.menuUrl}</span>
</div>
</td>
<td className="px-4 py-2">
{renderActions(f, 'flex items-center justify-end gap-1')}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)} )}
</div> </div>

View file

@ -87,7 +87,7 @@ const WizardStepComponent = ({
<KindCard <KindCard
active={!isCustom} active={!isCustom}
icon={<FaTable />} icon={<FaTable />}
title={translate('::App.WizardStepComponent.ListComponent')} title={translate('::App.Listforms.Listform')}
description={translate('::App.WizardStepComponent.ListComponentHint')} description={translate('::App.WizardStepComponent.ListComponentHint')}
onSelect={() => onKindChange(WizardComponentKindEnum.List)} onSelect={() => onKindChange(WizardComponentKindEnum.List)}
/> />

View file

@ -69,10 +69,13 @@ function buildLogSteps(
translate: (key: string, params?: Record<string, string | number>) => string, translate: (key: string, params?: Record<string, string | number>) => string,
): Omit<LogEntry, 'status'>[] { ): Omit<LogEntry, 'status'>[] {
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0) const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
// Menuye eklenmeyen wizard'da menu kaydi uretilmez; adim listesinde de yer almaz.
const withoutMenuStep = (steps: Omit<LogEntry, 'status'>[]) =>
values.createMenu === false ? steps.filter((step) => step.id !== 2) : steps
// Custom yolunda veri kaynağı / ListForm / alan üretimi hiç çalışmaz. // Custom yolunda veri kaynağı / ListForm / alan üretimi hiç çalışmaz.
if (values.componentKind === WizardComponentKindEnum.Custom) { if (values.componentKind === WizardComponentKindEnum.Custom) {
return [ return withoutMenuStep([
{ id: 1, label: translate('::App.Step4Log.ValidatingConfig') }, { id: 1, label: translate('::App.Step4Log.ValidatingConfig') },
{ {
id: 2, id: 2,
@ -99,10 +102,10 @@ function buildLogSteps(
}, },
{ id: 8, label: translate('::App.Step4Log.Deploying') }, { id: 8, label: translate('::App.Step4Log.Deploying') },
{ id: 9, label: translate('::App.Step4Log.Completed') }, { id: 9, label: translate('::App.Step4Log.Completed') },
] ])
} }
return [ return withoutMenuStep([
{ id: 1, label: translate('::App.Step4Log.ValidatingConfig') }, { id: 1, label: translate('::App.Step4Log.ValidatingConfig') },
{ {
id: 2, id: 2,
@ -140,7 +143,7 @@ function buildLogSteps(
}, },
{ id: 8, label: translate('::App.Step4Log.Deploying') }, { id: 8, label: translate('::App.Step4Log.Deploying') },
{ id: 9, label: translate('::App.Step4Log.Completed') }, { id: 9, label: translate('::App.Step4Log.Completed') },
] ])
} }
// ─── Mini-components ────────────────────────────────────────────────────────── // ─── Mini-components ──────────────────────────────────────────────────────────

View file

@ -1,4 +1,5 @@
import { Button, FormItem, Input, Notification, Select, toast } from '@/components/ui' import { Button, FormItem, Input, Notification, Select, Switcher, toast } from '@/components/ui'
import classNames from 'classnames'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
import { MenuItem } from '@/proxy/menus/menu' import { MenuItem } from '@/proxy/menus/menu'
@ -366,6 +367,8 @@ export interface WizardStepMenuProps {
menuTree: MenuTreeNode[] menuTree: MenuTreeNode[]
isLoadingMenu: boolean isLoadingMenu: boolean
onMenuParentChange: (code: string) => void onMenuParentChange: (code: string) => void
/** Menuye eklenip eklenmeyecegi; false iken menuye ozel alanlar hic sorulmaz. */
onCreateMenuChange: (createMenu: boolean) => void
onClearMenuParent: () => void onClearMenuParent: () => void
onMenuCreated: (menu: { onMenuCreated: (menu: {
code: string code: string
@ -399,6 +402,7 @@ const WizardStepMenu = ({
menuTree, menuTree,
isLoadingMenu, isLoadingMenu,
onMenuParentChange, onMenuParentChange,
onCreateMenuChange,
onClearMenuParent, onClearMenuParent,
onMenuCreated, onMenuCreated,
onReloadMenu, onReloadMenu,
@ -446,16 +450,22 @@ const WizardStepMenu = ({
? `/admin/list/${values.menuCode}` ? `/admin/list/${values.menuCode}`
: '' : ''
/**
* Menuye eklenmeyen wizard: ListForm, permission ve dil anahtarlari uretilir, menu kaydi
* uretilmez. Bu durumda ust menu / ikon / sira / adres alanlari hic sorulmaz.
*/
const createMenu = values.createMenu !== false
const step1Missing = [ const step1Missing = [
!wizardName && translate('::App.WizardStep1.WizardName'), !wizardName && translate('::App.WizardStep1.WizardName'),
!values.menuCode && translate('::App.WizardStep1.MenuCode'), !values.menuCode && translate('::App.WizardStep1.MenuCode'),
// Yup şeması menuOrder >= 1 istiyor; başlangıç değeri 0 olduğu için // Yup şeması menuOrder >= 1 istiyor; başlangıç değeri 0 olduğu için
// burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu. // burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu.
!(Number(values.menuOrder) >= 1) && translate('::App.WizardStep1.MenuOrder'), createMenu && !(Number(values.menuOrder) >= 1) && translate('::App.WizardStep1.MenuOrder'),
!values.permissionGroupName && translate('::App.WizardStep1.PermissionGroupName'), !values.permissionGroupName && translate('::App.WizardStep1.PermissionGroupName'),
!values.languageTextMenuEn && translate('::App.WizardStep4.MenuEn'), !values.languageTextMenuEn && translate('::App.WizardStep4.MenuEn'),
!values.languageTextMenuTr && translate('::App.WizardStep4.MenuTr'), !values.languageTextMenuTr && translate('::App.WizardStep4.MenuTr'),
!values.menuIcon && translate('::App.WizardStep4.MenuIcon'), createMenu && !values.menuIcon && translate('::App.WizardStep4.MenuIcon'),
].filter(Boolean) as string[] ].filter(Boolean) as string[]
// Geçersiz ya da çakışan ad deploy'da menü/permission kayıtlarını bozar; adım burada durur. // Geçersiz ya da çakışan ad deploy'da menü/permission kayıtlarını bozar; adım burada durur.
const step1CanGo = step1Missing.length === 0 && !wizardNameError const step1CanGo = step1Missing.length === 0 && !wizardNameError
@ -474,169 +484,144 @@ const WizardStepMenu = ({
</span> </span>
} }
> >
<Input <div className="flex items-center gap-3">
type="text" <Input
autoComplete="off" type="text"
placeholder="Routes, Products, Orders" autoComplete="off"
value={wizardName} placeholder="Routes, Products, Orders"
autoFocus value={wizardName}
invalid={!!wizardNameError} autoFocus
onChange={(e) => onWizardNameChange(e.target.value.replace(/\s/g, ''))} invalid={!!wizardNameError}
/> onChange={(e) => onWizardNameChange(e.target.value.replace(/\s/g, ''))}
/>
{/* Menuye eklenecek mi: kapaliyken menuye ozel alanlarin hicbiri sorulmaz. */}
<div
className="flex shrink-0 items-center gap-2 whitespace-nowrap text-sm text-gray-600 dark:text-gray-300"
title={translate('::App.WizardStep1.CreateMenuHint')}
>
{/* Switcher kontrollu kullanimda onChange'e mevcut degeri geri verdigi icin
yeni deger burada mevcut degerin tersi olarak hesaplanir. */}
<Switcher checked={createMenu} onChange={() => onCreateMenuChange(!createMenu)} />
<span className="cursor-pointer" onClick={() => onCreateMenuChange(!createMenu)}>
{translate('::App.WizardStep1.CreateMenu')}
</span>
</div>
</div>
</FormItem> </FormItem>
{/* Menu Code / Menu Text / Permission Group — 2-column grid */} {/* 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"> <div
{/* Col 1 */} className={classNames('grid grid-cols-1 gap-x-6 gap-y-4', createMenu && 'md:grid-cols-2')}
<div className="flex flex-col"> >
{/* Menu Parent */} {/* Col 1 — yalnizca menuye eklenen wizard'da: ust menu agaci */}
<FormItem {createMenu && (
/* FormItem'in içerik kutusu da esnesin ki ağaç sağ kolon yüksekliğini doldursun. */ <div className="flex flex-col">
className="flex flex-1 flex-col [&>div]:flex [&>div]:min-h-0 [&>div]:flex-1 [&>div]:flex-col" {/* Menu Parent */}
label={translate('::App.Platform.MenuParent')} <FormItem
invalid={false} /* FormItem'in içerik kutusu da esnesin ki ağaç sağ kolon yüksekliğini doldursun. */
errorMessage={undefined} className="flex flex-1 flex-col [&>div]:flex [&>div]:min-h-0 [&>div]:flex-1 [&>div]:flex-col"
extra={ label={translate('::App.Platform.MenuParent')}
<div className="flex items-center gap-2 ml-3"> /* extra icerigi label satirinin tamamini kaplasin ki menu kodu en saga yaslanabilsin. */
<Button labelClass="flex w-full items-center [&>span]:min-w-0 [&>span]:flex-1"
type="button" invalid={false}
onClick={() => { errorMessage={undefined}
setMenuBeingEdited(null) extra={
setMenuDialogParentCode( /* Butonlar <label> icinde oldugu icin label'in bos alanina tiklamak ilk
values.menuParentCode kontrolu (Add) tetikliyordu; varsayilan davranis burada durdurulur. */
? findRootCode(rawMenuItems, values.menuParentCode) <div
: '', className="ml-3 flex flex-1 items-center gap-2"
) onClick={(e) => e.preventDefault()}
setMenuDialogOpen(true)
}}
size="xs"
variant="solid"
icon={<FaPlus className="text-xs" />}
> >
<span className="whitespace-nowrap">{translate('::App.Platform.Add')}</span>
</Button>
{values.menuParentCode && (
<Button <Button
type="button" type="button"
onClick={(e) => { onClick={() => {
e.stopPropagation() setMenuBeingEdited(null)
e.preventDefault() setMenuDialogParentCode(
onClearMenuParent() values.menuParentCode
? findRootCode(rawMenuItems, values.menuParentCode)
: '',
)
setMenuDialogOpen(true)
}} }}
size="xs" size="xs"
variant="default" variant="solid"
icon={<FaTimes className="text-xs" />} icon={<FaPlus className="text-xs" />}
className="!inline-flex !h-auto !items-center !justify-center gap-1 !rounded !px-2 !py-0.5 text-xs text-gray-500 whitespace-nowrap hover:border-red-400 hover:text-red-500 dark:border-gray-600"
> >
{translate('::App.Platform.ClearSelection')} <span className="whitespace-nowrap">{translate('::App.Platform.Add')}</span>
</Button> </Button>
)} {values.menuParentCode && (
</div> <Button
} type="button"
> onClick={(e) => {
<Field name="menuParentCode"> e.stopPropagation()
{() => ( e.preventDefault()
<MenuTreeInline onClearMenuParent()
value={values.menuParentCode} }}
onChange={onMenuParentChange} size="xs"
nodes={menuTree} variant="default"
rawItems={rawMenuItems} icon={<FaTimes className="text-xs" />}
isLoading={isLoadingMenu} className="!inline-flex !h-auto !items-center !justify-center gap-1 !rounded !px-2 !py-0.5 text-xs text-gray-500 whitespace-nowrap hover:border-red-400 hover:text-red-500 dark:border-gray-600"
invalid={false} >
onReload={onReloadMenu} {translate('::App.Platform.ClearSelection')}
onEditMenu={handleEditMenu} </Button>
initialExpanded={ )}
values.menuParentCode {/* Menu Code: kullanici duzenlemez, parent satirinin en saginda gosterilir. */}
? getAncestorCodes(rawMenuItems, values.menuParentCode) <Field
: undefined type="text"
} autoComplete="off"
/> name="menuCode"
)} placeholder="App.Wizard.Routes"
</Field> component={Input}
</FormItem> size="xs"
disabled={true}
<MenuAddDialog title={translate('::App.Platform.MenuCodeHint')}
isOpen={menuDialogOpen} className="ml-auto max-w-[16rem] font-mono text-xs"
onClose={closeMenuDialog} />
initialParentCode={menuDialogParentCode} </div>
initialOrder={menuDialogInitialOrder}
rawItems={rawMenuItems}
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) >
}} <Field name="menuParentCode">
/> {() => (
</div> <MenuTreeInline
value={values.menuParentCode}
onChange={onMenuParentChange}
nodes={menuTree}
rawItems={rawMenuItems}
isLoading={isLoadingMenu}
invalid={false}
onReload={onReloadMenu}
onEditMenu={handleEditMenu}
initialExpanded={
values.menuParentCode
? getAncestorCodes(rawMenuItems, values.menuParentCode)
: undefined
}
/>
)}
</Field>
</FormItem>
<MenuAddDialog
isOpen={menuDialogOpen}
onClose={closeMenuDialog}
initialParentCode={menuDialogParentCode}
initialOrder={menuDialogInitialOrder}
rawItems={rawMenuItems}
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>
)}
{/* Col 2 */} {/* Col 2 */}
<div> <div>
{/* Menu Code */}
<FormItem
label={translate('::App.WizardStep1.MenuCode')}
invalid={!!(errors.menuCode && touched.menuCode)}
errorMessage={errors.menuCode}
asterisk={true}
extra={
<span className="text-xs ml-2 text-gray-400">
{translate('::App.Platform.MenuCodeHint')}
</span>
}
>
<Field
type="text"
autoComplete="off"
name="menuCode"
placeholder="App.Wizard.Routes, App.Wizard.Products, App.Wizard.Orders"
component={Input}
disabled={true}
/>
</FormItem>
{/* Menu Order */}
<FormItem
label={translate('::App.WizardStep1.MenuOrder')}
asterisk={true}
invalid={!!(errors.menuOrder && touched.menuOrder)}
errorMessage={errors.menuOrder}
extra={
<span className="text-xs ml-2 text-gray-400">
{translate('::App.WizardStep1.MenuOrderHint')}
</span>
}
>
<Field
type="number"
min={1}
step={1}
autoComplete="off"
name="menuOrder"
component={Input}
/>
</FormItem>
{/* Menu Icon */}
<FormItem
label={translate('::App.WizardStep1.MenuIcon')}
asterisk={true}
invalid={!!(errors.menuIcon && touched.menuIcon)}
errorMessage={errors.menuIcon}
>
<Field name="menuIcon">
{({ field, form }: FieldProps<string>) => (
<IconPickerField
value={field.value}
onChange={(key) => form.setFieldValue(field.name, key)}
invalid={!!(errors.menuIcon && touched.menuIcon)}
/>
)}
</Field>
</FormItem>
{/* Menu Text (En) */} {/* Menu Text (En) */}
<FormItem <FormItem
label={translate('::App.Platform.DisplayNameEnglish')} label={translate('::App.Platform.DisplayNameEnglish')}
@ -669,7 +654,7 @@ const WizardStepMenu = ({
/> />
</FormItem> </FormItem>
{/* Permission Group Name */} {/* Permission grubu menuden bagimsizdir: izinler her iki durumda da uretilir. */}
<FormItem <FormItem
label={translate('::App.WizardStep1.PermissionGroupName')} label={translate('::App.WizardStep1.PermissionGroupName')}
invalid={!!(errors.permissionGroupName && touched.permissionGroupName)} invalid={!!(errors.permissionGroupName && touched.permissionGroupName)}
@ -702,23 +687,67 @@ const WizardStepMenu = ({
</Field> </Field>
</FormItem> </FormItem>
{/* Deploy sonrası menü ikonunun açacağı adres; seçilen yola göre değişir. */} {createMenu && (
<FormItem <>
label={translate('::App.WizardStepComponent.MenuUrl')} {/* Menu Icon */}
extra={ <FormItem
<span className="ml-2 text-xs text-gray-400"> label={translate('::App.WizardStep1.MenuIcon')}
{translate( asterisk={true}
isCustomComponent invalid={!!(errors.menuIcon && touched.menuIcon)}
? '::App.WizardStepComponent.MenuUrlFromComponent' errorMessage={errors.menuIcon}
: '::App.WizardStepComponent.MenuUrlFromList', >
)} <Field name="menuIcon">
</span> {({ field, form }: FieldProps<string>) => (
} <IconPickerField
> value={field.value}
<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"> onChange={(key) => form.setFieldValue(field.name, key)}
{menuUrl || '—'} invalid={!!(errors.menuIcon && touched.menuIcon)}
</div> />
</FormItem> )}
</Field>
</FormItem>
{/* Menu Order */}
<FormItem
label={translate('::App.WizardStep1.MenuOrder')}
asterisk={true}
invalid={!!(errors.menuOrder && touched.menuOrder)}
errorMessage={errors.menuOrder}
extra={
<span className="text-xs ml-2 text-gray-400">
{translate('::App.WizardStep1.MenuOrderHint')}
</span>
}
>
<Field
type="number"
min={1}
step={1}
autoComplete="off"
name="menuOrder"
component={Input}
/>
</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>
</div> </div>