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 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>
public string CustomComponentName { get; set; }

View file

@ -133,7 +133,11 @@ public class ListFormWizardAppService(
}
//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))
{
await repoPermGroup.InsertAsync(new PermissionGroupDefinitionRecord(GuidGenerator.Create(), groupName, groupName), autoSave: false);
@ -208,6 +212,15 @@ public class ListFormWizardAppService(
await permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: false);
}
// Menunun acacagi adres: Custom yolunda CustomComponent'in kendi rotasi (bu rota
// mapCustomComponentRoutes tarafindan zaten kayitlidir), aksi halde ListForm adresi.
// Seed dosyasindan da gorulebilmesi icin input'a geri yazilir; menu uretilmese de
// ListForm'un adresi degismedigi icin ayni sekilde hesaplanir.
input.MenuUrl = isCustomComponent ? customRoutePath : WizardConsts.MenuUrl(code);
// Menusuz wizard: menu (ve ust menu) kaydi hic uretilmez, geri kalan her sey aynidir.
if (input.CreateMenu)
{
//Menu Parent
var menuQueryable = await repoMenu.GetQueryableAsync();
var menuParent = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == input.MenuParentCode));
@ -242,11 +255,7 @@ 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 menuUrl = input.MenuUrl;
var existingMenu = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == code));
if (existingMenu == null)
{
@ -272,6 +281,7 @@ public class ListFormWizardAppService(
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.

View file

@ -1503,8 +1503,8 @@
{
"resourceName": "Platform",
"key": "App.Listforms.Listform",
"en": "Listforms",
"tr": "Listformlar"
"en": "ListForm Components",
"tr": "List Form Bileşenleri"
},
{
"resourceName": "Platform",
@ -16326,6 +16326,18 @@
"en": "Automatically assigned, editable",
"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",
"key": "App.Platform.Add",
@ -16836,12 +16848,6 @@
"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",

View file

@ -797,16 +797,6 @@
"RequiredPermissionName": "App.DataSource",
"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",
"Code": "App.Notifications",
@ -978,6 +968,16 @@
"RequiredPermissionName": "App.DeveloperKit.Components",
"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",
"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
// 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
? 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);
if (alreadySeeded)
@ -181,7 +185,9 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
}
// 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))
{
await _repoPermGroup.InsertAsync(
@ -249,6 +255,16 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
// await _permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: true);
// }
// Menunun acacagi adres; menu uretilmese de seed dosyasindaki degeri korunur.
input.MenuUrl = isCustomComponent
? customRoutePath
: !string.IsNullOrWhiteSpace(input.MenuUrl)
? input.MenuUrl.Trim()
: WizardConsts.MenuUrl(code);
// Menusuz wizard: menu (ve ust menu) kaydi hic uretilmez.
if (input.CreateMenu)
{
// Menu Parent
var menuQueryable = await _repoMenu.GetQueryableAsync();
var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode);
@ -276,14 +292,7 @@ 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 menuUrl = input.MenuUrl;
var existingMenu = await _repoMenu.FirstOrDefaultAsync(a => a.Code == code);
if (existingMenu == null)
{
@ -308,6 +317,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
existingMenu.Url = menuUrl;
await _repoMenu.UpdateAsync(existingMenu, autoSave: true);
}
}
// Custom yolunda ListForm ekosistemi üretilmez.
if (isCustomComponent)

View file

@ -47,6 +47,12 @@ export interface ListFormWizardDto {
menuCode: string
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
/** Menünün açtığı adres; Custom yolunda seçilen bileşenin rota yolu. */
menuUrl: string

View file

@ -55,6 +55,7 @@ const initialValues: ListFormWizardDto = {
listFormCode: '',
menuCode: '',
menuOrder: 0,
createMenu: true,
componentKind: WizardComponentKindEnum.List,
customComponentName: '',
menuUrl: '',
@ -174,7 +175,16 @@ const initialValues: ListFormWizardDto = {
const step1ValidationSchema = Yup.object().shape({
wizardName: 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(),
languageTextMenuEn: Yup.string().required(),
languageTextMenuTr: Yup.string().required(),
@ -595,6 +605,8 @@ const Wizard = () => {
wizardName: w.wizardName ?? '',
listFormCode: w.listFormCode ?? '',
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.
componentKind: w.componentKind ?? WizardComponentKindEnum.List,
customComponentName: w.customComponentName ?? '',
@ -830,6 +842,23 @@ const Wizard = () => {
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 formik = formikRef.current
const parentChanged = formik?.values.menuParentCode !== code
@ -1180,6 +1209,7 @@ const Wizard = () => {
menuTree={menuTree}
isLoadingMenu={isLoadingMenu}
onMenuParentChange={handleMenuParentChange}
onCreateMenuChange={handleCreateMenuChange}
onClearMenuParent={() => {
formikRef.current?.setFieldValue('menuParentCode', '')
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"
>
<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">
{/* 1. satır: wizard adı */}
<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}
>
{f.wizardName || f.fileName}
@ -368,14 +368,9 @@ const WizardFileManager = () => {
<div className="text-xs text-gray-400 truncate font-mono" title={f.fileName}>
{f.fileName}
</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">
<div className="flex flex-wrap pt-1 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',
@ -388,10 +383,15 @@ const WizardFileManager = () => {
{translate(
isCustom
? '::App.WizardStepComponent.CustomComponent'
: '::App.WizardStepComponent.ListComponent',
: '::App.Listforms.Listform',
)}
</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. */}
{!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">
@ -409,6 +409,8 @@ const WizardFileManager = () => {
</span>
)}
</div>
</div>
</div>
{renderActions(
f,
@ -419,37 +421,63 @@ const WizardFileManager = () => {
})}
</div>
) : (
<div className="space-y-2">
<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 (
<div
<tr
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"
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}
>
<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 */}
<div className="flex flex-wrap items-center gap-1.5 text-xs">
</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 font-medium',
'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',
@ -459,31 +487,27 @@ const WizardFileManager = () => {
{translate(
isCustom
? '::App.WizardStepComponent.CustomComponent'
: '::App.WizardStepComponent.ListComponent',
: '::App.Listforms.Listform',
)}
</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" />
</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>
</span>
)}
</div>
</div>
</div>
{renderActions(f, 'flex items-center gap-1 shrink-0 sm:ml-3')}
</div>
</td>
<td className="px-4 py-2">
{renderActions(f, 'flex items-center justify-end gap-1')}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>

View file

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

View file

@ -69,10 +69,13 @@ 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)
// 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.
if (values.componentKind === WizardComponentKindEnum.Custom) {
return [
return withoutMenuStep([
{ id: 1, label: translate('::App.Step4Log.ValidatingConfig') },
{
id: 2,
@ -99,10 +102,10 @@ function buildLogSteps(
},
{ id: 8, label: translate('::App.Step4Log.Deploying') },
{ id: 9, label: translate('::App.Step4Log.Completed') },
]
])
}
return [
return withoutMenuStep([
{ id: 1, label: translate('::App.Step4Log.ValidatingConfig') },
{
id: 2,
@ -140,7 +143,7 @@ function buildLogSteps(
},
{ id: 8, label: translate('::App.Step4Log.Deploying') },
{ id: 9, label: translate('::App.Step4Log.Completed') },
]
])
}
// ─── 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 { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
import { MenuItem } from '@/proxy/menus/menu'
@ -366,6 +367,8 @@ export interface WizardStepMenuProps {
menuTree: MenuTreeNode[]
isLoadingMenu: boolean
onMenuParentChange: (code: string) => void
/** Menuye eklenip eklenmeyecegi; false iken menuye ozel alanlar hic sorulmaz. */
onCreateMenuChange: (createMenu: boolean) => void
onClearMenuParent: () => void
onMenuCreated: (menu: {
code: string
@ -399,6 +402,7 @@ const WizardStepMenu = ({
menuTree,
isLoadingMenu,
onMenuParentChange,
onCreateMenuChange,
onClearMenuParent,
onMenuCreated,
onReloadMenu,
@ -446,16 +450,22 @@ const WizardStepMenu = ({
? `/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 = [
!wizardName && translate('::App.WizardStep1.WizardName'),
!values.menuCode && translate('::App.WizardStep1.MenuCode'),
// Yup şeması menuOrder >= 1 istiyor; başlangıç değeri 0 olduğu için
// burada kontrol edilmezse "Next" aktif görünüp sessizce çalışmıyordu.
!(Number(values.menuOrder) >= 1) && translate('::App.WizardStep1.MenuOrder'),
createMenu && !(Number(values.menuOrder) >= 1) && translate('::App.WizardStep1.MenuOrder'),
!values.permissionGroupName && translate('::App.WizardStep1.PermissionGroupName'),
!values.languageTextMenuEn && translate('::App.WizardStep4.MenuEn'),
!values.languageTextMenuTr && translate('::App.WizardStep4.MenuTr'),
!values.menuIcon && translate('::App.WizardStep4.MenuIcon'),
createMenu && !values.menuIcon && translate('::App.WizardStep4.MenuIcon'),
].filter(Boolean) as string[]
// 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
@ -474,6 +484,7 @@ const WizardStepMenu = ({
</span>
}
>
<div className="flex items-center gap-3">
<Input
type="text"
autoComplete="off"
@ -483,21 +494,44 @@ const WizardStepMenu = ({
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>
{/* 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
className={classNames('grid grid-cols-1 gap-x-6 gap-y-4', createMenu && 'md:grid-cols-2')}
>
{/* Col 1 — yalnizca menuye eklenen wizard'da: ust menu agaci */}
{createMenu && (
<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')}
/* extra icerigi label satirinin tamamini kaplasin ki menu kodu en saga yaslanabilsin. */
labelClass="flex w-full items-center [&>span]:min-w-0 [&>span]:flex-1"
invalid={false}
errorMessage={undefined}
extra={
<div className="flex items-center gap-2 ml-3">
/* Butonlar <label> icinde oldugu icin label'in bos alanina tiklamak ilk
kontrolu (Add) tetikliyordu; varsayilan davranis burada durdurulur. */
<div
className="ml-3 flex flex-1 items-center gap-2"
onClick={(e) => e.preventDefault()}
>
<Button
type="button"
onClick={() => {
@ -531,6 +565,18 @@ const WizardStepMenu = ({
{translate('::App.Platform.ClearSelection')}
</Button>
)}
{/* Menu Code: kullanici duzenlemez, parent satirinin en saginda gosterilir. */}
<Field
type="text"
autoComplete="off"
name="menuCode"
placeholder="App.Wizard.Routes"
component={Input}
size="xs"
disabled={true}
title={translate('::App.Platform.MenuCodeHint')}
className="ml-auto max-w-[16rem] font-mono text-xs"
/>
</div>
}
>
@ -572,71 +618,10 @@ const WizardStepMenu = ({
}}
/>
</div>
)}
{/* Col 2 */}
<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) */}
<FormItem
label={translate('::App.Platform.DisplayNameEnglish')}
@ -669,7 +654,7 @@ const WizardStepMenu = ({
/>
</FormItem>
{/* Permission Group Name */}
{/* Permission grubu menuden bagimsizdir: izinler her iki durumda da uretilir. */}
<FormItem
label={translate('::App.WizardStep1.PermissionGroupName')}
invalid={!!(errors.permissionGroupName && touched.permissionGroupName)}
@ -702,6 +687,48 @@ const WizardStepMenu = ({
</Field>
</FormItem>
{createMenu && (
<>
{/* 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 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')}
@ -719,6 +746,8 @@ const WizardStepMenu = ({
{menuUrl || '—'}
</div>
</FormItem>
</>
)}
</div>
</div>