2026-03-03 07:34:25 +00:00
|
|
|
|
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'
|
2026-08-16 15:30:07 +00:00
|
|
|
|
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
|
|
|
|
|
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
2026-08-18 08:00:53 +00:00
|
|
|
|
import { FaChevronDown, FaEdit, FaPlus } from 'react-icons/fa'
|
2026-03-03 07:34:25 +00:00
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
2026-08-11 11:09:58 +00:00
|
|
|
|
import { useConfig } from '@/components/ui/ConfigProvider'
|
|
|
|
|
|
import { useForm } from '@/components/ui/Form/context'
|
2026-03-03 07:34:25 +00:00
|
|
|
|
|
|
|
|
|
|
const menuService = new MenuService()
|
|
|
|
|
|
|
|
|
|
|
|
// ─── IconPickerField ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
interface IconPickerFieldProps {
|
|
|
|
|
|
value: string
|
|
|
|
|
|
onChange: (iconKey: string) => void
|
|
|
|
|
|
invalid?: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ICON_PAGE_SIZE = 100
|
|
|
|
|
|
|
|
|
|
|
|
export function IconPickerField({ value, onChange, invalid }: IconPickerFieldProps) {
|
2026-08-16 15:30:07 +00:00
|
|
|
|
const navigationIcon = useNavigationIcons()
|
|
|
|
|
|
// Sözlük asenkron dolduğu için liste yükleme bittiğinde yeniden türetilir.
|
|
|
|
|
|
const allIconEntries = useMemo(() => Object.entries(navigationIcon), [navigationIcon])
|
2026-03-03 07:34:25 +00:00
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
|
|
const [search, setSearch] = useState('')
|
|
|
|
|
|
const [limit, setLimit] = useState(ICON_PAGE_SIZE)
|
|
|
|
|
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
2026-08-11 11:09:58 +00:00
|
|
|
|
// Tetikleyici yüksekliği, komşu Input/Select alanlarıyla aynı olsun diye
|
|
|
|
|
|
// Input bileşeniyle birebir aynı kaynaktan (form context → global config) okunur.
|
|
|
|
|
|
const formSize = useForm()?.size
|
|
|
|
|
|
const { controlSize: defaultControlSize } = useConfig()
|
|
|
|
|
|
const controlSize = formSize ?? defaultControlSize
|
|
|
|
|
|
|
2026-03-03 07:34:25 +00:00
|
|
|
|
const SelectedIcon = value ? navigationIcon[value] : null
|
|
|
|
|
|
const filtered = search.trim()
|
2026-08-16 15:30:07 +00:00
|
|
|
|
? allIconEntries.filter(([key]) => key.toLowerCase().includes(search.toLowerCase()))
|
|
|
|
|
|
: allIconEntries
|
2026-03-03 07:34:25 +00:00
|
|
|
|
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">
|
2026-08-11 11:09:58 +00:00
|
|
|
|
{/*
|
|
|
|
|
|
Tetikleyici, platformun `.input` sınıfını kullanır; böylece kenarlık, köşe
|
|
|
|
|
|
yarıçapı, dolgu ve açık/koyu tema arka planı diğer form alanlarıyla birebir
|
|
|
|
|
|
aynı olur. Metin renkleri `!` ile verilir: Button bileşeni variant'ına göre
|
|
|
|
|
|
kendi `text-*` sınıfını basıyor ve aynı özgüllükte olduğu için eziyordu.
|
2026-08-13 11:34:43 +00:00
|
|
|
|
Aynı sebeple kenarlık/arka plan da `!` ile verilir: `plain` variant
|
|
|
|
|
|
`bg-transparent border border-transparent` bastığı için `.input` kenarlığı
|
|
|
|
|
|
eziliyor ve alan açık temada görünmez oluyordu (yalnızca hover'da beliriyordu).
|
2026-08-11 11:09:58 +00:00
|
|
|
|
*/}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
2026-03-03 07:34:25 +00:00
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setOpen((v) => !v)}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
variant="plain"
|
2026-08-13 11:34:43 +00:00
|
|
|
|
shape="round"
|
2026-08-11 11:09:58 +00:00
|
|
|
|
size={controlSize}
|
2026-08-13 11:34:43 +00:00
|
|
|
|
className={`input flex items-center gap-2 !justify-start !px-3 text-left text-sm !text-gray-700 dark:!text-gray-100
|
|
|
|
|
|
!bg-white dark:!bg-transparent !border !border-gray-300 dark:!border-gray-600 hover:!border-indigo-400
|
|
|
|
|
|
${invalid ? 'input-invalid !border-red-500' : ''}`}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
>
|
|
|
|
|
|
{SelectedIcon ? (
|
|
|
|
|
|
<span className="flex items-center gap-2 flex-1 truncate">
|
|
|
|
|
|
<SelectedIcon className="text-xl shrink-0" />
|
2026-08-11 11:09:58 +00:00
|
|
|
|
<span className="truncate">{value}</span>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
2026-08-18 08:00:53 +00:00
|
|
|
|
<span className="flex-1 !text-gray-400">{translate('::App.WizardStep1.SelectIcon')}</span>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
)}
|
|
|
|
|
|
<FaChevronDown
|
2026-08-11 11:09:58 +00:00
|
|
|
|
className={`shrink-0 text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
/>
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
|
|
|
|
|
|
{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]) => (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
2026-03-03 07:34:25 +00:00
|
|
|
|
key={key}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
title={key}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
onChange(key)
|
|
|
|
|
|
setOpen(false)
|
|
|
|
|
|
setSearch('')
|
|
|
|
|
|
}}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
|
|
|
|
|
className={`!flex !h-10 !w-full !min-w-0 !items-center !justify-center !rounded !px-0 text-xl transition-colors
|
2026-03-03 07:34:25 +00:00
|
|
|
|
${value === key ? 'bg-indigo-500 text-white' : 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300'}`}
|
|
|
|
|
|
>
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Icon className="h-5 w-5" />
|
|
|
|
|
|
</Button>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
))}
|
|
|
|
|
|
</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>
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
2026-03-03 07:34:25 +00:00
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setLimit((l) => l + ICON_PAGE_SIZE)}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
size="xs"
|
2026-06-26 08:59:13 +00:00
|
|
|
|
variant="solid"
|
2026-03-03 07:34:25 +00:00
|
|
|
|
>
|
2026-06-26 08:59:13 +00:00
|
|
|
|
{translate('::Abp.Identity.ActivityLogs.LoadMore')}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── MenuAddDialog ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
export interface MenuAddDialogProps {
|
|
|
|
|
|
isOpen: boolean
|
|
|
|
|
|
onClose: () => void
|
|
|
|
|
|
initialParentCode: string
|
|
|
|
|
|
initialOrder: number
|
|
|
|
|
|
rawItems: (MenuItem & { id?: string })[]
|
2026-08-18 08:00:53 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Dolu ise dialog düzenleme modunda açılır ve menünün tüm alanları değiştirilebilir.
|
|
|
|
|
|
* Boş bırakılırsa yeni menü oluşturma modudur.
|
|
|
|
|
|
*/
|
|
|
|
|
|
editMenu?: (MenuItem & { id?: string }) | null
|
2026-06-05 09:30:40 +00:00
|
|
|
|
onSaved: (menu: {
|
|
|
|
|
|
code: string
|
|
|
|
|
|
parentCode?: string
|
|
|
|
|
|
menuTextEn: string
|
|
|
|
|
|
menuTextTr: string
|
|
|
|
|
|
icon?: string
|
|
|
|
|
|
shortName?: string
|
|
|
|
|
|
}) => void | Promise<void>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-18 08:00:53 +00:00
|
|
|
|
/** Düzenleme modunda menü kendi alt ağacına taşınamaz; aksi halde döngü oluşur. */
|
|
|
|
|
|
function collectDescendantCodes(
|
|
|
|
|
|
items: (MenuItem & { id?: string })[],
|
|
|
|
|
|
rootCode: string,
|
|
|
|
|
|
): Set<string> {
|
|
|
|
|
|
const result = new Set<string>([rootCode])
|
|
|
|
|
|
let grew = true
|
|
|
|
|
|
while (grew) {
|
|
|
|
|
|
grew = false
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
|
if (!item.code || result.has(item.code)) continue
|
|
|
|
|
|
if (item.parentCode && result.has(item.parentCode)) {
|
|
|
|
|
|
result.add(item.code)
|
|
|
|
|
|
grew = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const emptyForm = (parentCode: string, order: number) => ({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
code: '',
|
|
|
|
|
|
menuTextEn: '',
|
|
|
|
|
|
menuTextTr: '',
|
|
|
|
|
|
parentCode,
|
|
|
|
|
|
icon: '',
|
|
|
|
|
|
shortName: '',
|
|
|
|
|
|
order,
|
|
|
|
|
|
url: '',
|
|
|
|
|
|
isDisabled: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-03 07:34:25 +00:00
|
|
|
|
export function MenuAddDialog({
|
|
|
|
|
|
isOpen,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
initialParentCode,
|
|
|
|
|
|
initialOrder,
|
|
|
|
|
|
rawItems,
|
2026-08-18 08:00:53 +00:00
|
|
|
|
editMenu,
|
2026-03-03 07:34:25 +00:00
|
|
|
|
onSaved,
|
|
|
|
|
|
}: MenuAddDialogProps) {
|
2026-08-18 08:00:53 +00:00
|
|
|
|
const isEditMode = !!editMenu?.id
|
|
|
|
|
|
const [form, setForm] = useState(() => emptyForm(initialParentCode, initialOrder))
|
2026-03-03 07:34:25 +00:00
|
|
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-08-18 08:00:53 +00:00
|
|
|
|
if (!isOpen) return
|
|
|
|
|
|
if (editMenu?.id) {
|
2026-03-03 07:34:25 +00:00
|
|
|
|
setForm({
|
2026-08-18 08:00:53 +00:00
|
|
|
|
name: editMenu.code ?? '',
|
|
|
|
|
|
code: editMenu.code ?? '',
|
|
|
|
|
|
menuTextEn: editMenu.menuTextEn ?? '',
|
|
|
|
|
|
menuTextTr: editMenu.menuTextTr ?? '',
|
|
|
|
|
|
parentCode: editMenu.parentCode ?? '',
|
|
|
|
|
|
icon: editMenu.icon ?? '',
|
|
|
|
|
|
shortName: editMenu.shortName ?? '',
|
|
|
|
|
|
order: editMenu.order ?? 0,
|
|
|
|
|
|
url: editMenu.url ?? '',
|
|
|
|
|
|
isDisabled: editMenu.isDisabled ?? false,
|
2026-03-03 07:34:25 +00:00
|
|
|
|
})
|
2026-08-18 08:00:53 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setForm(emptyForm(initialParentCode, initialOrder))
|
|
|
|
|
|
}, [isOpen, initialParentCode, initialOrder, editMenu])
|
2026-03-03 07:34:25 +00:00
|
|
|
|
|
2026-08-18 08:00:53 +00:00
|
|
|
|
// Kök menülerde kısa ad zorunlu; alt menülerde isteğe bağlıdır.
|
2026-03-03 07:34:25 +00:00
|
|
|
|
const shortNameRequired = !form.parentCode.trim()
|
2026-08-18 08:00:53 +00:00
|
|
|
|
|
|
|
|
|
|
// Düzenlenen menü, kendisi veya alt ağacındaki bir menünün altına taşınamaz.
|
|
|
|
|
|
const parentOptions = useMemo(() => {
|
|
|
|
|
|
if (!isEditMode) return []
|
|
|
|
|
|
const blocked = collectDescendantCodes(rawItems, editMenu?.code ?? '')
|
|
|
|
|
|
return rawItems
|
|
|
|
|
|
.filter((item) => item.code && !blocked.has(item.code))
|
|
|
|
|
|
.map((item) => ({ code: item.code!, label: item.code! }))
|
|
|
|
|
|
.sort((a, b) => a.label.localeCompare(b.label))
|
|
|
|
|
|
}, [isEditMode, rawItems, editMenu])
|
|
|
|
|
|
|
2026-03-03 07:34:25 +00:00
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
|
if (!form.code.trim() || !form.menuTextEn.trim()) return
|
|
|
|
|
|
if (shortNameRequired && !form.shortName.trim()) return
|
|
|
|
|
|
setSaving(true)
|
|
|
|
|
|
try {
|
2026-06-05 09:30:40 +00:00
|
|
|
|
const savedMenu = {
|
2026-03-03 07:34:25 +00:00
|
|
|
|
code: form.code.trim(),
|
2026-08-18 08:00:53 +00:00
|
|
|
|
// Görünen ad dil anahtarıdır; menü kodu ile aynı kalır.
|
2026-03-03 07:34:25 +00:00
|
|
|
|
displayName: form.code.trim(),
|
|
|
|
|
|
parentCode: form.parentCode.trim() || undefined,
|
|
|
|
|
|
icon: form.icon || undefined,
|
|
|
|
|
|
shortName: form.shortName.trim() || undefined,
|
|
|
|
|
|
order: form.order,
|
2026-08-18 08:00:53 +00:00
|
|
|
|
isDisabled: form.isDisabled,
|
|
|
|
|
|
url: form.url.trim() || undefined,
|
2026-03-03 07:34:25 +00:00
|
|
|
|
menuTextTr: form.menuTextTr.trim(),
|
|
|
|
|
|
menuTextEn: form.menuTextEn.trim(),
|
2026-06-05 09:30:40 +00:00
|
|
|
|
} as MenuDto
|
2026-03-03 07:34:25 +00:00
|
|
|
|
|
2026-08-18 08:00:53 +00:00
|
|
|
|
if (isEditMode) {
|
|
|
|
|
|
// Kod ve görünen ad dışındaki tüm alanlar güncellenir; sunucu kodu korur.
|
|
|
|
|
|
await menuService.updateWithLanguageKeyText(editMenu!.id!, {
|
|
|
|
|
|
...(editMenu as MenuDto),
|
|
|
|
|
|
...savedMenu,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await menuService.createWithLanguageKeyText(savedMenu)
|
|
|
|
|
|
}
|
2026-06-05 09:30:40 +00:00
|
|
|
|
|
|
|
|
|
|
await onSaved({
|
|
|
|
|
|
code: savedMenu.code!,
|
|
|
|
|
|
parentCode: savedMenu.parentCode,
|
|
|
|
|
|
menuTextEn: savedMenu.menuTextEn!,
|
|
|
|
|
|
menuTextTr: savedMenu.menuTextTr!,
|
|
|
|
|
|
icon: savedMenu.icon,
|
|
|
|
|
|
shortName: savedMenu.shortName,
|
|
|
|
|
|
})
|
2026-03-03 07:34:25 +00:00
|
|
|
|
onClose()
|
|
|
|
|
|
} catch (e: any) {
|
2026-07-08 12:34:13 +00:00
|
|
|
|
toast.push(<Notification title={e.message} type="danger" />, {
|
|
|
|
|
|
placement: 'bottom-end',
|
|
|
|
|
|
})
|
2026-03-03 07:34:25 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setSaving(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fieldCls =
|
2026-06-27 18:38:36 +00:00
|
|
|
|
'h-9 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 outline-none focus:border-indigo-400 w-full'
|
2026-03-03 07:34:25 +00:00
|
|
|
|
const disabledCls =
|
2026-06-27 18:38:36 +00:00
|
|
|
|
'h-9 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'
|
2026-03-03 07:34:25 +00:00
|
|
|
|
const labelCls = 'text-xs font-medium text-gray-500 dark:text-gray-400 mb-1'
|
|
|
|
|
|
|
2026-03-21 18:51:27 +00:00
|
|
|
|
const toCodeToken = (value: string) => value.replace(/\s+/g, '')
|
|
|
|
|
|
const toSpacedLabel = (value: string) =>
|
|
|
|
|
|
value
|
|
|
|
|
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
|
|
|
|
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
|
2026-03-03 07:34:25 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<Dialog isOpen={isOpen} onClose={onClose} onRequestClose={onClose} width={680}>
|
2026-05-08 20:29:06 +00:00
|
|
|
|
<div className="flex flex-col gap-5 p-1">
|
2026-03-03 07:34:25 +00:00
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<div className="flex items-center gap-2 pb-1 border-b border-gray-100 dark:border-gray-700">
|
2026-08-18 08:00:53 +00:00
|
|
|
|
{isEditMode ? (
|
|
|
|
|
|
<FaEdit className="text-indigo-500 text-sm" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<FaPlus className="text-green-500 text-sm" />
|
|
|
|
|
|
)}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
<h5 className="text-base font-semibold text-gray-800 dark:text-gray-100">
|
2026-08-18 08:00:53 +00:00
|
|
|
|
{isEditMode
|
|
|
|
|
|
? translate('::App.Platform.EditMenu')
|
|
|
|
|
|
: translate('::App.Platform.AddNewMenu')}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
</h5>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Row 1 — Name | Code */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
|
<label className={labelCls}>
|
2026-08-14 11:32:31 +00:00
|
|
|
|
{translate('::App.Listform.ListformField.Name')}{' '}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
<span className="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
2026-08-18 08:00:53 +00:00
|
|
|
|
{/* Ad, kodu ve dil anahtarını türetir; düzenlemede kod sabit olduğu için kilitlidir. */}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
<input
|
2026-08-18 08:00:53 +00:00
|
|
|
|
autoFocus={!isEditMode}
|
|
|
|
|
|
disabled={isEditMode}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
value={form.name}
|
2026-03-21 18:51:27 +00:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const codeToken = toCodeToken(e.target.value)
|
|
|
|
|
|
const spacedLabel = toSpacedLabel(codeToken)
|
|
|
|
|
|
|
2026-03-03 07:34:25 +00:00
|
|
|
|
setForm((p) => ({
|
|
|
|
|
|
...p,
|
2026-03-21 18:51:27 +00:00
|
|
|
|
name: codeToken,
|
|
|
|
|
|
code: `App.Wizard.${codeToken}`,
|
|
|
|
|
|
menuTextEn: spacedLabel,
|
|
|
|
|
|
menuTextTr: spacedLabel,
|
|
|
|
|
|
shortName: codeToken.substring(0, 3),
|
2026-03-03 07:34:25 +00:00
|
|
|
|
}))
|
2026-03-21 18:51:27 +00:00
|
|
|
|
}}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
placeholder="MyMenu"
|
2026-08-18 08:00:53 +00:00
|
|
|
|
className={isEditMode ? disabledCls : fieldCls}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
|
<label className={labelCls}>
|
2026-08-14 11:32:31 +00:00
|
|
|
|
{translate('::App.Platform.Code')} <span className="text-red-500">*</span>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
</label>
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<input
|
|
|
|
|
|
value={form.code}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
placeholder="App.Wizard.MyMenu"
|
|
|
|
|
|
className={disabledCls}
|
|
|
|
|
|
/>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Row 2 — Icon (full width) */}
|
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
|
<label className={labelCls}>
|
2026-03-29 08:59:07 +00:00
|
|
|
|
{translate('::App.Listform.ListformField.Icon')} <span className="text-red-500">*</span>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
</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}>
|
2026-08-14 14:03:34 +00:00
|
|
|
|
{translate('::App.Platform.DisplayNameEnglish')}{' '}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
<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}>
|
2026-08-14 14:03:34 +00:00
|
|
|
|
{translate('::App.Platform.DisplayNameTurkish')}{' '}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
<span className="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
value={form.menuTextTr}
|
|
|
|
|
|
onChange={(e) => setForm((p) => ({ ...p, menuTextTr: e.target.value }))}
|
2026-08-14 11:32:31 +00:00
|
|
|
|
placeholder={translate('::App.Menu.NamePlaceholder')}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
className={fieldCls}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Row 4 — Menu Parent | Order */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="flex flex-col">
|
2026-08-14 14:03:34 +00:00
|
|
|
|
<label className={labelCls}>{translate('::App.Platform.MenuParent')}</label>
|
2026-08-18 08:00:53 +00:00
|
|
|
|
{isEditMode ? (
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={form.parentCode}
|
|
|
|
|
|
onChange={(e) => setForm((p) => ({ ...p, parentCode: e.target.value }))}
|
|
|
|
|
|
className={fieldCls}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">{translate('::App.WizardStep1.RootMenu')}</option>
|
|
|
|
|
|
{parentOptions.map((option) => (
|
|
|
|
|
|
<option key={option.code} value={option.code}>
|
|
|
|
|
|
{option.label}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<input disabled value={form.parentCode} className={disabledCls} />
|
|
|
|
|
|
)}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-col">
|
2026-08-14 11:32:31 +00:00
|
|
|
|
<label className={labelCls}>{translate('::App.Listform.ListformField.Order')}</label>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
<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}>
|
2026-08-14 14:03:34 +00:00
|
|
|
|
{translate('::App.WizardStep1.ShortName')}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
{shortNameRequired ? (
|
|
|
|
|
|
<span className="text-red-500 ml-0.5">*</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="ml-1 text-gray-400 font-normal">
|
2026-08-14 14:03:34 +00:00
|
|
|
|
({translate('::App.WizardStep1.Optional')})
|
2026-03-03 07:34:25 +00:00
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
value={form.shortName}
|
|
|
|
|
|
onChange={(e) => setForm((p) => ({ ...p, shortName: e.target.value }))}
|
|
|
|
|
|
placeholder="Sas, Finance, Hr…"
|
|
|
|
|
|
className={`${fieldCls}`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{shortNameRequired && (
|
2026-08-18 08:00:53 +00:00
|
|
|
|
<p className="text-xs text-gray-400 mt-1">{translate('::App.Platform.MenuCodeHint')}</p>
|
2026-03-03 07:34:25 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-18 08:00:53 +00:00
|
|
|
|
{/* Row 6 — Url | Durum (yalnızca düzenlemede) */}
|
|
|
|
|
|
{isEditMode && (
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
|
<label className={labelCls}>{translate('::App.Platform.OpenUrl')}</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
value={form.url}
|
|
|
|
|
|
onChange={(e) => setForm((p) => ({ ...p, url: e.target.value }))}
|
|
|
|
|
|
placeholder="/admin/list/App.Wizard.MyMenu"
|
|
|
|
|
|
className={`${fieldCls} font-mono`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
|
<label className={labelCls}>{translate('::App.Platform.Status')}</label>
|
|
|
|
|
|
<label className="flex h-9 cursor-pointer items-center gap-2">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={!form.isDisabled}
|
|
|
|
|
|
onChange={(e) => setForm((p) => ({ ...p, isDisabled: !e.target.checked }))}
|
|
|
|
|
|
className="h-4 w-4 accent-indigo-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-sm text-gray-700 dark:text-gray-200">
|
|
|
|
|
|
{translate(
|
|
|
|
|
|
form.isDisabled
|
|
|
|
|
|
? '::App.Platform.Passive'
|
|
|
|
|
|
: '::App.Listform.ListformField.IsActive',
|
|
|
|
|
|
)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-03 07:34:25 +00:00
|
|
|
|
{/* Footer */}
|
|
|
|
|
|
<div className="flex justify-end gap-2 pt-1 border-t border-gray-100 dark:border-gray-700">
|
2026-06-05 09:30:40 +00:00
|
|
|
|
<Button type="button" size="sm" variant="plain" onClick={onClose}>
|
2026-08-14 14:03:34 +00:00
|
|
|
|
{translate('::App.Platform.Cancel')}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
2026-06-05 09:30:40 +00:00
|
|
|
|
type="button"
|
2026-03-03 07:34:25 +00:00
|
|
|
|
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}
|
|
|
|
|
|
>
|
2026-08-14 14:03:34 +00:00
|
|
|
|
{translate('::App.Platform.Save')}
|
2026-03-03 07:34:25 +00:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|