MenuAddDialog eklendi
This commit is contained in:
parent
490a73dde8
commit
c5ad419a27
4 changed files with 1111 additions and 804 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
355
ui/src/views/shared/MenuAddDialog.tsx
Normal file
355
ui/src/views/shared/MenuAddDialog.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue