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,6 +212,15 @@ public class ListFormWizardAppService(
await permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: false); 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 //Menu Parent
var menuQueryable = await repoMenu.GetQueryableAsync(); var menuQueryable = await repoMenu.GetQueryableAsync();
var menuParent = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == input.MenuParentCode)); 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 maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0;
var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1; var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1;
input.MenuOrder = menuOrder; input.MenuOrder = menuOrder;
// Menünün açacağı adres: Custom yolunda CustomComponent'in kendi rotası (bu rota var menuUrl = input.MenuUrl;
// 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)); var existingMenu = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == code));
if (existingMenu == null) if (existingMenu == null)
{ {
@ -272,6 +281,7 @@ public class ListFormWizardAppService(
existingMenu.Url = menuUrl; existingMenu.Url = menuUrl;
await repoMenu.UpdateAsync(existingMenu, autoSave: false); await repoMenu.UpdateAsync(existingMenu, autoSave: false);
} }
}
// Custom yolunda ListForm ekosistemi üretilmez: veri kaynağı, ListForm, alanlar, // Custom yolunda ListForm ekosistemi üretilmez: veri kaynağı, ListForm, alanlar,
// subform/widget/workflow kayıtlarının hiçbirine ihtiyaç yoktur. // subform/widget/workflow kayıtlarının hiçbirine ihtiyaç yoktur.

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
? seedFile.Wizard.CreateMenu
? await _repoMenu.AnyAsync(a => a.Code == ResolveMenuCode(seedFile.Wizard)) ? 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,6 +255,16 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
// await _permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: true); // 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 // Menu Parent
var menuQueryable = await _repoMenu.GetQueryableAsync(); var menuQueryable = await _repoMenu.GetQueryableAsync();
var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode); 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 maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0;
var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1; var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1;
input.MenuOrder = menuOrder; input.MenuOrder = menuOrder;
// Menünün açacağı adres. Custom yolunda normalize edilmis rota, List yolunda var menuUrl = input.MenuUrl;
// 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); var existingMenu = await _repoMenu.FirstOrDefaultAsync(a => a.Code == code);
if (existingMenu == null) if (existingMenu == null)
{ {
@ -308,6 +317,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
existingMenu.Url = menuUrl; existingMenu.Url = menuUrl;
await _repoMenu.UpdateAsync(existingMenu, autoSave: true); await _repoMenu.UpdateAsync(existingMenu, autoSave: true);
} }
}
// Custom yolunda ListForm ekosistemi üretilmez. // Custom yolunda ListForm ekosistemi üretilmez.
if (isCustomComponent) if (isCustomComponent)

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,14 +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 */} {/* 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',
@ -388,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">
@ -409,6 +409,8 @@ const WizardFileManager = () => {
</span> </span>
)} )}
</div> </div>
</div>
</div>
{renderActions( {renderActions(
f, f,
@ -419,37 +421,63 @@ const WizardFileManager = () => {
})} })}
</div> </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) => { {filteredFiles.map((f) => {
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
const isCustom = f.componentKind === WizardComponentKindEnum.Custom const isCustom = f.componentKind === WizardComponentKindEnum.Custom
return ( return (
<div <tr
key={f.fileName} 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} {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> </span>
</div> </div>
</td>
{/* 3. satır: yol ayrımı (Custom / List), layout ve menü adresi */} <td
<div className="flex flex-wrap items-center gap-1.5 text-xs"> 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 <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 text-xs font-medium',
isCustom isCustom
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300' ? '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', : 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
@ -459,31 +487,27 @@ const WizardFileManager = () => {
{translate( {translate(
isCustom isCustom
? '::App.WizardStepComponent.CustomComponent' ? '::App.WizardStepComponent.CustomComponent'
: '::App.WizardStepComponent.ListComponent', : '::App.Listforms.Listform',
)} )}
</span> </span>
</td>
{/* Custom yolunda layout kavramı yoktur. */} <td
{!isCustom && f.defaultLayout && ( className="px-4 py-2 font-mono text-gray-600 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"> title={f.menuUrl}
{f.defaultLayout} >
</span> <div className="flex items-center gap-1 min-w-0">
)} {f.menuUrl && <FaLink className="shrink-0 text-gray-400" />}
{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 className="truncate">{f.menuUrl}</span>
</span>
)}
</div>
</div>
</div>
{renderActions(f, 'flex items-center gap-1 shrink-0 sm:ml-3')}
</div> </div>
</td>
<td className="px-4 py-2">
{renderActions(f, 'flex items-center justify-end gap-1')}
</td>
</tr>
) )
})} })}
</tbody>
</table>
</div> </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,6 +484,7 @@ const WizardStepMenu = ({
</span> </span>
} }
> >
<div className="flex items-center gap-3">
<Input <Input
type="text" type="text"
autoComplete="off" autoComplete="off"
@ -483,21 +494,44 @@ const WizardStepMenu = ({
invalid={!!wizardNameError} invalid={!!wizardNameError}
onChange={(e) => onWizardNameChange(e.target.value.replace(/\s/g, ''))} 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')}
>
{/* Col 1 — yalnizca menuye eklenen wizard'da: ust menu agaci */}
{createMenu && (
<div className="flex flex-col"> <div className="flex flex-col">
{/* Menu Parent */} {/* Menu Parent */}
<FormItem <FormItem
/* FormItem'in içerik kutusu da esnesin ki ağaç sağ kolon yüksekliğini doldursun. */ /* 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" className="flex flex-1 flex-col [&>div]:flex [&>div]:min-h-0 [&>div]:flex-1 [&>div]:flex-col"
label={translate('::App.Platform.MenuParent')} 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} invalid={false}
errorMessage={undefined} errorMessage={undefined}
extra={ 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 <Button
type="button" type="button"
onClick={() => { onClick={() => {
@ -531,6 +565,18 @@ const WizardStepMenu = ({
{translate('::App.Platform.ClearSelection')} {translate('::App.Platform.ClearSelection')}
</Button> </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> </div>
} }
> >
@ -572,71 +618,10 @@ const WizardStepMenu = ({
}} }}
/> />
</div> </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,6 +687,48 @@ const WizardStepMenu = ({
</Field> </Field>
</FormItem> </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. */} {/* Deploy sonrası menü ikonunun açacağı adres; seçilen yola göre değişir. */}
<FormItem <FormItem
label={translate('::App.WizardStepComponent.MenuUrl')} label={translate('::App.WizardStepComponent.MenuUrl')}
@ -719,6 +746,8 @@ const WizardStepMenu = ({
{menuUrl || '—'} {menuUrl || '—'}
</div> </div>
</FormItem> </FormItem>
</>
)}
</div> </div>
</div> </div>