MenuAddDialog eklendi

This commit is contained in:
Sedat ÖZTÜRK 2026-03-03 10:34:25 +03:00
parent 490a73dde8
commit c5ad419a27
4 changed files with 1111 additions and 804 deletions

View file

@ -15836,6 +15836,12 @@
"en": "Used to generate ListForm Code and Menu Code", "en": "Used to generate ListForm Code and Menu Code",
"tr": "ListForm Kodu ve Menü Kodu oluşturmak için kullanılır" "tr": "ListForm Kodu ve Menü Kodu oluşturmak için kullanılır"
}, },
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step1.Optional",
"en": "Optional",
"tr": "İsteğe Bağlı"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.Wizard.Step1.WizardName", "key": "ListForms.Wizard.Step1.WizardName",

View file

@ -1,4 +1,4 @@
import { Button, Dialog, FormItem, Input, Notification, Select, toast } from '@/components/ui' import { Button, FormItem, Input, Notification, Select, toast } from '@/components/ui'
import { ListFormWizardDto } from '@/proxy/admin/list-form/models' import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
import { MenuDto } from '@/proxy/menus/models' import { MenuDto } from '@/proxy/menus/models'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
@ -6,7 +6,7 @@ import navigationIcon from '@/proxy/menus/navigation-icon.config'
import { MenuItem } from '@/proxy/menus/menu' import { MenuItem } from '@/proxy/menus/menu'
import { MenuService } from '@/services/menu.service' import { MenuService } from '@/services/menu.service'
import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik' import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
import { useEffect, useRef, useState } from 'react' import { useEffect, useState } from 'react'
import CreatableSelect from 'react-select/creatable' import CreatableSelect from 'react-select/creatable'
import { import {
FaArrowRight, FaArrowRight,
@ -19,6 +19,7 @@ import {
FaTrash, FaTrash,
} from 'react-icons/fa' } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { IconPickerField, MenuAddDialog } from '@/views/shared/MenuAddDialog'
// ─── Types (exported for Wizard.tsx) ───────────────────────────────────────── // ─── Types (exported for Wizard.tsx) ─────────────────────────────────────────
@ -371,332 +372,6 @@ function MenuTreeInline({
) )
} }
// ─── IconPickerField ──────────────────────────────────────────────────────────
interface IconPickerFieldProps {
value: string
onChange: (iconKey: string) => void
invalid?: boolean
}
const ALL_ICON_ENTRIES = Object.entries(navigationIcon)
const ICON_PAGE_SIZE = 100
function IconPickerField({ value, onChange, invalid }: IconPickerFieldProps) {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const [limit, setLimit] = useState(ICON_PAGE_SIZE)
const wrapperRef = useRef<HTMLDivElement>(null)
const SelectedIcon = value ? navigationIcon[value] : null
const filtered = search.trim()
? ALL_ICON_ENTRIES.filter(([key]) => key.toLowerCase().includes(search.toLowerCase()))
: ALL_ICON_ENTRIES
const displayed = filtered.slice(0, limit)
const hasMore = displayed.length < filtered.length
const { translate } = useLocalization()
useEffect(() => {
setLimit(ICON_PAGE_SIZE)
}, [search])
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) setOpen(false)
}
if (open) document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [open])
return (
<div ref={wrapperRef} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className={`flex items-center gap-2 w-full h-11 px-3 py-2 rounded-lg border text-sm text-left transition-colors
${invalid ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:border-indigo-400`}
>
{SelectedIcon ? (
<span className="flex items-center gap-2 flex-1 truncate">
<SelectedIcon className="text-xl shrink-0" />
<span className="text-xs text-gray-500 truncate">{value}</span>
</span>
) : (
<span className="text-gray-400 flex-1">{translate('::ListForms.Wizard.Step1.SelectIcon') || 'Select icon...'}</span>
)}
<FaChevronDown
className={`text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`}
/>
</button>
{open && (
<div className="absolute z-50 mt-1 left-0 w-[360px] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-xl">
<div className="p-2 border-b border-gray-100 dark:border-gray-700 flex items-center gap-2">
<input
autoFocus
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search icons… (FaHome, FcSettings)"
className="flex-1 px-2 py-1.5 text-sm rounded border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 outline-none focus:border-indigo-400"
/>
<span className="text-xs text-gray-400 shrink-0">{filtered.length} icons</span>
</div>
<div className="grid grid-cols-10 gap-0.5 p-2 max-h-[208px] overflow-y-auto">
{displayed.map(([key, Icon]) => (
<button
key={key}
type="button"
title={key}
onClick={() => {
onChange(key)
setOpen(false)
setSearch('')
}}
className={`flex items-center justify-center h-10 w-full rounded text-xl transition-colors
${value === key ? 'bg-indigo-500 text-white' : 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300'}`}
>
<Icon />
</button>
))}
</div>
{hasMore && (
<div className="px-3 py-1.5 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between">
<span className="text-xs text-gray-400">
{displayed.length} / {filtered.length}
</span>
<button
type="button"
onClick={() => setLimit((l) => l + ICON_PAGE_SIZE)}
className="text-xs px-3 py-1 rounded bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/50 font-medium"
>
Load More
</button>
</div>
)}
</div>
)}
</div>
)
}
// ─── MenuAddDialog ────────────────────────────────────────────────────────────
interface MenuAddDialogProps {
isOpen: boolean
onClose: () => void
initialParentCode: string
initialOrder: number
rawItems: (MenuItem & { id?: string })[]
onSaved: () => void
}
function MenuAddDialog({
isOpen,
onClose,
initialParentCode,
initialOrder,
rawItems,
onSaved,
}: MenuAddDialogProps) {
const [form, setForm] = useState({
name: '',
code: '',
menuTextEn: '',
menuTextTr: '',
parentCode: initialParentCode,
icon: '',
shortName: '',
order: initialOrder,
})
const [saving, setSaving] = useState(false)
const { translate } = useLocalization()
useEffect(() => {
if (isOpen)
setForm({
name: '',
code: '',
menuTextEn: '',
menuTextTr: '',
parentCode: initialParentCode,
icon: '',
shortName: '',
order: initialOrder,
})
}, [isOpen, initialParentCode, initialOrder])
const handleSave = async () => {
if (!form.code.trim() || !form.menuTextEn.trim()) return
setSaving(true)
try {
// Menü oluşturuluyor
await menuService.createWithLanguageKeyText({
code: form.code.trim(),
displayName: form.code.trim(),
parentCode: form.parentCode.trim() || undefined,
icon: form.icon || undefined,
shortName: form.shortName.trim() || undefined,
order: form.order,
isDisabled: false,
menuTextTr: form.menuTextTr.trim(),
menuTextEn: form.menuTextEn.trim(),
} as MenuDto)
onSaved()
onClose()
} catch (e: any) {
toast.push(<Notification title={e.message} type="danger" />, { placement: 'top-end' })
} finally {
setSaving(false)
}
}
// suppress unused warning — rawItems kept for future use
void rawItems
const fieldCls =
'h-11 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 outline-none focus:border-indigo-400 w-full'
const disabledCls =
'h-11 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700 text-sm text-gray-400 dark:text-gray-500 cursor-not-allowed w-full'
const labelCls = 'text-xs font-medium text-gray-500 dark:text-gray-400 mb-1'
return (
<Dialog isOpen={isOpen} onClose={onClose} onRequestClose={onClose} width={680}>
<div className="flex flex-col gap-5 p-5">
{/* Header */}
<div className="flex items-center gap-2 pb-1 border-b border-gray-100 dark:border-gray-700">
<FaPlus className="text-green-500 text-sm" />
<h5 className="text-base font-semibold text-gray-800 dark:text-gray-100">{translate('::ListForms.Wizard.Step1.AddNewMenu')}</h5>
</div>
{/* Row 1 — Name | Code */}
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<label className={labelCls}>
{translate('::ListForms.Wizard.Step1.Name') || 'Name'} <span className="text-red-500">*</span>
</label>
<input
autoFocus
value={form.name}
onChange={(e) =>
setForm((p) => ({
...p,
name: e.target.value.replace(/\s+/g, ''),
code: `App.Wizard.${e.target.value.replace(/\s+/g, '')}`,
}))
}
placeholder="MyMenu"
className={fieldCls}
/>
</div>
<div className="flex flex-col">
<label className={labelCls}>
{translate('::ListForms.Wizard.Step1.Code') || 'Code'} <span className="text-red-500">*</span>
</label>
<input
value={form.code}
disabled
placeholder="App.Wizard.MyMenu"
className={disabledCls}
/>
</div>
</div>
{/* Row 3 — Icon (full width) */}
<div className="flex flex-col">
<label className={labelCls}>
{translate('::ListForms.Wizard.Step4.Icon')} <span className="text-red-500">*</span>
</label>
<IconPickerField
value={form.icon}
onChange={(key) => setForm((p) => ({ ...p, icon: key }))}
/>
</div>
{/* Row 2 — Display Name EN | Display Name TR */}
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<label className={labelCls}>
{translate('::ListForms.Wizard.Step1.DisplayNameEnglish')} <span className="text-red-500">*</span>
</label>
<input
value={form.menuTextEn}
onChange={(e) => setForm((p) => ({ ...p, menuTextEn: e.target.value }))}
placeholder="My Menu"
className={fieldCls}
/>
</div>
<div className="flex flex-col">
<label className={labelCls}>
{translate('::ListForms.Wizard.Step1.DisplayNameTurkish')} <span className="text-red-500">*</span>
</label>
<input
value={form.menuTextTr}
onChange={(e) => setForm((p) => ({ ...p, menuTextTr: e.target.value }))}
placeholder="Menüm"
className={fieldCls}
/>
</div>
</div>
{/* Row 4 — Menu Parent | Order */}
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<label className={labelCls}>{translate('::ListForms.Wizard.Step1.MenuParent')}</label>
<input
disabled
value={form.parentCode || translate('::ListForms.Wizard.Step1.MainMenu') || '(Ana Menü)'}
className={disabledCls}
/>
</div>
<div className="flex flex-col">
<label className={labelCls}>{translate('::ListForms.Wizard.Step1.Order') || 'Sıra (Order)'}</label>
<input
type="number"
value={form.order}
onChange={(e) => setForm((p) => ({ ...p, order: Number(e.target.value) }))}
className={fieldCls}
/>
</div>
</div>
{/* Row 5 — Short Name (full width) */}
<div className="flex flex-col">
<label className={labelCls}>{translate('::ListForms.Wizard.Step1.ShortName')}</label>
<input
value={form.shortName}
onChange={(e) => setForm((p) => ({ ...p, shortName: e.target.value }))}
placeholder="My Menu (short)"
className={fieldCls}
/>
</div>
{/* Footer */}
<div className="flex justify-end gap-2 pt-1 border-t border-gray-100 dark:border-gray-700">
<Button size="sm" variant="plain" onClick={onClose}>
{translate('::ListForms.Wizard.Cancel') || 'İptal'}
</Button>
<Button
size="sm"
variant="solid"
loading={saving}
disabled={
!form.code.trim() ||
!form.menuTextEn.trim() ||
!form.menuTextTr.trim() ||
!form.icon.trim()
}
onClick={handleSave}
>
{translate('::ListForms.Wizard.Save') || 'Kaydet'}
</Button>
</div>
</div>
</Dialog>
)
}
// ─── WizardStep1 ────────────────────────────────────────────────────────────── // ─── WizardStep1 ──────────────────────────────────────────────────────────────
export interface WizardStep1Props { export interface WizardStep1Props {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,355 @@
import { Button, Dialog, Notification, toast } from '@/components/ui'
import { MenuDto } from '@/proxy/menus/models'
import { MenuItem } from '@/proxy/menus/menu'
import { MenuService } from '@/services/menu.service'
import navigationIcon from '@/proxy/menus/navigation-icon.config'
import { useEffect, useRef, useState } from 'react'
import { FaChevronDown, FaPlus } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
const menuService = new MenuService()
// ─── IconPickerField ──────────────────────────────────────────────────────────
interface IconPickerFieldProps {
value: string
onChange: (iconKey: string) => void
invalid?: boolean
}
const ALL_ICON_ENTRIES = Object.entries(navigationIcon)
const ICON_PAGE_SIZE = 100
export function IconPickerField({ value, onChange, invalid }: IconPickerFieldProps) {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const [limit, setLimit] = useState(ICON_PAGE_SIZE)
const wrapperRef = useRef<HTMLDivElement>(null)
const SelectedIcon = value ? navigationIcon[value] : null
const filtered = search.trim()
? ALL_ICON_ENTRIES.filter(([key]) => key.toLowerCase().includes(search.toLowerCase()))
: ALL_ICON_ENTRIES
const displayed = filtered.slice(0, limit)
const hasMore = displayed.length < filtered.length
const { translate } = useLocalization()
useEffect(() => {
setLimit(ICON_PAGE_SIZE)
}, [search])
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) setOpen(false)
}
if (open) document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [open])
return (
<div ref={wrapperRef} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className={`flex items-center gap-2 w-full h-11 px-3 py-2 rounded-lg border text-sm text-left transition-colors
${invalid ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:border-indigo-400`}
>
{SelectedIcon ? (
<span className="flex items-center gap-2 flex-1 truncate">
<SelectedIcon className="text-xl shrink-0" />
<span className="text-xs text-gray-500 truncate">{value}</span>
</span>
) : (
<span className="text-gray-400 flex-1">
{translate('::ListForms.Wizard.Step1.SelectIcon') || 'Select icon...'}
</span>
)}
<FaChevronDown
className={`text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`}
/>
</button>
{open && (
<div className="absolute z-50 mt-1 left-0 w-[360px] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-xl">
<div className="p-2 border-b border-gray-100 dark:border-gray-700 flex items-center gap-2">
<input
autoFocus
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search icons… (FaHome, FcSettings)"
className="flex-1 px-2 py-1.5 text-sm rounded border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 outline-none focus:border-indigo-400"
/>
<span className="text-xs text-gray-400 shrink-0">{filtered.length} icons</span>
</div>
<div className="grid grid-cols-10 gap-0.5 p-2 max-h-[208px] overflow-y-auto">
{displayed.map(([key, Icon]) => (
<button
key={key}
type="button"
title={key}
onClick={() => {
onChange(key)
setOpen(false)
setSearch('')
}}
className={`flex items-center justify-center h-10 w-full rounded text-xl transition-colors
${value === key ? 'bg-indigo-500 text-white' : 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300'}`}
>
<Icon />
</button>
))}
</div>
{hasMore && (
<div className="px-3 py-1.5 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between">
<span className="text-xs text-gray-400">
{displayed.length} / {filtered.length}
</span>
<button
type="button"
onClick={() => setLimit((l) => l + ICON_PAGE_SIZE)}
className="text-xs px-3 py-1 rounded bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/50 font-medium"
>
Load More
</button>
</div>
)}
</div>
)}
</div>
)
}
// ─── MenuAddDialog ────────────────────────────────────────────────────────────
export interface MenuAddDialogProps {
isOpen: boolean
onClose: () => void
initialParentCode: string
initialOrder: number
rawItems: (MenuItem & { id?: string })[]
onSaved: () => void
}
export function MenuAddDialog({
isOpen,
onClose,
initialParentCode,
initialOrder,
rawItems,
onSaved,
}: MenuAddDialogProps) {
const [form, setForm] = useState({
name: '',
code: '',
menuTextEn: '',
menuTextTr: '',
parentCode: initialParentCode,
icon: '',
shortName: '',
order: initialOrder,
})
const [saving, setSaving] = useState(false)
const { translate } = useLocalization()
useEffect(() => {
if (isOpen)
setForm({
name: '',
code: '',
menuTextEn: '',
menuTextTr: '',
parentCode: initialParentCode,
icon: '',
shortName: '',
order: initialOrder,
})
}, [isOpen, initialParentCode, initialOrder])
const shortNameRequired = !form.parentCode.trim()
console.log(form.parentCode.length)
const handleSave = async () => {
if (!form.code.trim() || !form.menuTextEn.trim()) return
if (shortNameRequired && !form.shortName.trim()) return
setSaving(true)
try {
await menuService.createWithLanguageKeyText({
code: form.code.trim(),
displayName: form.code.trim(),
parentCode: form.parentCode.trim() || undefined,
icon: form.icon || undefined,
shortName: form.shortName.trim() || undefined,
order: form.order,
isDisabled: false,
menuTextTr: form.menuTextTr.trim(),
menuTextEn: form.menuTextEn.trim(),
} as MenuDto)
onSaved()
onClose()
} catch (e: any) {
toast.push(<Notification title={e.message} type="danger" />, { placement: 'top-end' })
} finally {
setSaving(false)
}
}
// suppress unused warning — rawItems kept for future use
void rawItems
const fieldCls =
'h-11 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 outline-none focus:border-indigo-400 w-full'
const disabledCls =
'h-11 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700 text-sm text-gray-400 dark:text-gray-500 cursor-not-allowed w-full'
const labelCls = 'text-xs font-medium text-gray-500 dark:text-gray-400 mb-1'
return (
<Dialog isOpen={isOpen} onClose={onClose} onRequestClose={onClose} width={680}>
<div className="flex flex-col gap-5 p-5">
{/* Header */}
<div className="flex items-center gap-2 pb-1 border-b border-gray-100 dark:border-gray-700">
<FaPlus className="text-green-500 text-sm" />
<h5 className="text-base font-semibold text-gray-800 dark:text-gray-100">
{translate('::ListForms.Wizard.Step1.AddNewMenu')}
</h5>
</div>
{/* Row 1 — Name | Code */}
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<label className={labelCls}>
{translate('::App.Platform.Name') || 'Name'}{' '}
<span className="text-red-500">*</span>
</label>
<input
autoFocus
value={form.name}
onChange={(e) =>
setForm((p) => ({
...p,
name: e.target.value.replace(/\s+/g, ''),
code: `App.Wizard.${e.target.value.replace(/\s+/g, '')}`,
}))
}
placeholder="MyMenu"
className={fieldCls}
/>
</div>
<div className="flex flex-col">
<label className={labelCls}>
{translate('::App.Platform.Code') || 'Code'}{' '}
<span className="text-red-500">*</span>
</label>
<input value={form.code} disabled placeholder="App.Wizard.MyMenu" className={disabledCls} />
</div>
</div>
{/* Row 2 — Icon (full width) */}
<div className="flex flex-col">
<label className={labelCls}>
{translate('::ListForms.Wizard.Step4.Icon')} <span className="text-red-500">*</span>
</label>
<IconPickerField
value={form.icon}
onChange={(key) => setForm((p) => ({ ...p, icon: key }))}
/>
</div>
{/* Row 3 — Display Name EN | Display Name TR */}
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<label className={labelCls}>
{translate('::ListForms.Wizard.Step1.DisplayNameEnglish')}{' '}
<span className="text-red-500">*</span>
</label>
<input
value={form.menuTextEn}
onChange={(e) => setForm((p) => ({ ...p, menuTextEn: e.target.value }))}
placeholder="My Menu"
className={fieldCls}
/>
</div>
<div className="flex flex-col">
<label className={labelCls}>
{translate('::ListForms.Wizard.Step1.DisplayNameTurkish')}{' '}
<span className="text-red-500">*</span>
</label>
<input
value={form.menuTextTr}
onChange={(e) => setForm((p) => ({ ...p, menuTextTr: e.target.value }))}
placeholder="Menüm"
className={fieldCls}
/>
</div>
</div>
{/* Row 4 — Menu Parent | Order */}
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<label className={labelCls}>{translate('::ListForms.Wizard.Step1.MenuParent')}</label>
<input disabled value={form.parentCode} className={disabledCls} />
</div>
<div className="flex flex-col">
<label className={labelCls}>
{translate('::ListForms.Wizard.Step1.Order') || 'Sıra (Order)'}
</label>
<input
type="number"
value={form.order}
onChange={(e) => setForm((p) => ({ ...p, order: Number(e.target.value) }))}
className={fieldCls}
/>
</div>
</div>
{/* Row 5 — Short Name (full width) */}
<div className="flex flex-col">
<label className={labelCls}>
{translate('::ListForms.Wizard.Step1.ShortName')}
{shortNameRequired ? (
<span className="text-red-500 ml-0.5">*</span>
) : (
<span className="ml-1 text-gray-400 font-normal">
({translate('::ListForms.Wizard.Step1.Optional') || 'opsiyonel'})
</span>
)}
</label>
<input
value={form.shortName}
onChange={(e) => setForm((p) => ({ ...p, shortName: e.target.value }))}
placeholder="Sas, Finance, Hr…"
className={`${fieldCls}`}
/>
{shortNameRequired && (
<p className="text-xs text-gray-400 mt-1">
{translate('::ListForms.Wizard.Step1.ShortNameHint') ||
'Tablo prefix olarak kullanılır (örn. "Sas" → Sas_D_TableName)'}
</p>
)}
</div>
{/* Footer */}
<div className="flex justify-end gap-2 pt-1 border-t border-gray-100 dark:border-gray-700">
<Button size="sm" variant="plain" onClick={onClose}>
{translate('::Cancel') || 'İptal'}
</Button>
<Button
size="sm"
variant="solid"
loading={saving}
disabled={
!form.code.trim() ||
!form.menuTextEn.trim() ||
!form.menuTextTr.trim() ||
!form.icon.trim() ||
(shortNameRequired && !form.shortName.trim())
}
onClick={handleSave}
>
{translate('::App.Platform.Save') || 'Kaydet'}
</Button>
</div>
</div>
</Dialog>
)
}