sozsoft-platform/ui/src/views/menu/MenuItemComponent.tsx
2026-03-17 22:56:34 +03:00

360 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { MenuItem } from '@/proxy/menus/menu'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { MenuService } from '@/services/menu.service'
import navigationIcon from '@/proxy/menus/navigation-icon.config'
import { FaQuestionCircle } from 'react-icons/fa'
import {
Button,
Dialog,
FormContainer,
FormItem,
Input,
Switcher,
Notification,
toast,
Select,
} from '@/components/ui'
import { Field, FieldProps, Form, Formik } from 'formik'
import { SelectBoxOption } from '@/types/shared'
import * as Yup from 'yup'
import { FaExternalLinkAlt, FaPlus, FaTrashAlt } from 'react-icons/fa'
import { MenuDto } from '@/proxy/menus/models'
interface MenuItemComponentProps {
item: MenuItem
isDesignMode: boolean
depth: number
children?: React.ReactNode
isDragOverlay?: boolean
refetch: () => void
permissions: SelectBoxOption[]
}
export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
item,
isDesignMode,
depth,
children,
isDragOverlay = false,
refetch,
permissions,
}) => {
const { translate } = useLocalization()
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: item.id || '',
data: {
type: 'menu-item',
item,
},
disabled: !isDesignMode,
})
const validationSchema = Yup.object().shape({
code: Yup.string().required('Code is required'),
displayName: Yup.string().required('Display Name is required'),
order: Yup.number().typeError('Order must be a number').required('Order is required'),
url: Yup.string().nullable(),
icon: Yup.string().nullable(),
cssClass: Yup.string().nullable(),
requiredPermissionName: Yup.string().nullable(),
target: Yup.string().nullable(),
elementId: Yup.string().nullable(),
isDisabled: Yup.boolean(),
})
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
}
const [isExpanded, setIsExpanded] = useState(true)
const [isModalOpen, setIsModalOpen] = useState(false)
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create')
const getCreateInitialValues = (): Partial<MenuDto> => ({
code: '',
displayName: '',
order: (item.children?.length || 0) + 1,
parentCode: item.code,
url: '',
icon: '',
cssClass: '',
requiredPermissionName: '',
target: '',
isDisabled: false,
elementId: '',
})
const getEditInitialValues = (): Partial<MenuDto> => ({
id: item.id,
code: item.code || '',
displayName: item.displayName || '',
order: item.order,
parentCode: item.parentCode || '',
url: item.url || '',
icon: item.icon || '',
cssClass: item.cssClass || '',
requiredPermissionName: item.requiredPermissionName || '',
target: item.target || '',
isDisabled: item.isDisabled ?? false,
elementId: item.elementId || '',
})
const [formData, setFormData] = useState<Partial<MenuDto>>(getCreateInitialValues())
const toggleExpanded = () => {
if (!isDesignMode) setIsExpanded(!isExpanded)
}
const openCreateModal = () => {
setModalMode('create')
setFormData(getCreateInitialValues())
setIsModalOpen(true)
}
const openEditModal = (event: React.MouseEvent) => {
if (!isDesignMode) return
event.stopPropagation()
setModalMode('edit')
setFormData(getEditInitialValues())
setIsModalOpen(true)
}
const handleDelete = async () => {
const confirmed = window.confirm(`Delete "${item.displayName}"?`)
if (!confirmed) return
const menuService = new MenuService()
await menuService.delete(item.id!)
refetch()
}
return (
<div className="select-none">
<div
ref={setNodeRef}
style={style}
className={`
flex items-center gap-1 p-1 rounded-lg transition-all duration-200 group min-h-[30px]
${isDesignMode ? 'cursor-move hover:bg-blue-50 border border-transparent hover:border-blue-200' : 'cursor-pointer hover:bg-gray-50'}
${isDragOverlay ? 'shadow-lg bg-white border border-blue-300 z-50' : ''}
${isDragging ? 'opacity-50' : ''}
${item.children && item.children.length > 0 ? 'bg-blue-50' : depth === 0 ? 'bg-white' : 'bg-gray-50'}
`}
{...(isDesignMode ? { ...attributes, ...listeners } : { onClick: toggleExpanded })}
>
{isDesignMode && (
<div className="flex gap-2 items-center mr-2">
<button onClick={openCreateModal} title="New Item">
<FaPlus size={16} className="text-green-600 hover:text-green-800" />
</button>
<button onClick={handleDelete} title="Delete Item">
<FaTrashAlt size={16} className="text-red-600 hover:text-red-800" />
</button>
</div>
)}
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex-shrink-0 text-gray-600 text-xl">
{navigationIcon[item.icon || ''] ? (
React.createElement(navigationIcon[item.icon || ''], { className: 'text-gray-400' })
) : (
<FaQuestionCircle className="text-gray-400" />
)}
</div>
<button
type="button"
onClick={openEditModal}
className={`
truncate text-gray-800 leading-6 text-sm text-left
${item.children && item.children.length > 0 ? 'font-semibold' : 'font-normal'}
${isDesignMode ? 'hover:text-blue-600' : ''}
`}
>
{translate('::' + item.displayName)}
</button>
{item.url && <FaExternalLinkAlt size={12} className="flex-shrink-0 text-gray-400" />}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{isDesignMode && (
<div className="flex items-center gap-2 text-xs text-gray-500">
<span className="bg-gray-200 px-2 py-1 rounded">#{item.order}</span>
</div>
)}
{item.children && item.children.length > 0 && (
<span className="text-xs text-gray-500 bg-blue-100 px-2 py-1 rounded-full">
{item.children.length}
</span>
)}
</div>
</div>
{children && (!isDesignMode ? isExpanded : true) && <div className="mt-1">{children}</div>}
{isModalOpen && (
<Dialog
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onRequestClose={() => setIsModalOpen(false)}
width={600}
>
<h5 className="mb-4">
{modalMode === 'edit' ? translate('::Edit Menu Item') : translate('::New Item')}
</h5>
<Formik
validationSchema={validationSchema}
initialValues={formData}
enableReinitialize
onSubmit={async (values, { setSubmitting }) => {
try {
const menuService = new MenuService()
if (modalMode === 'edit' && item.id) {
await menuService.update(item.id, values as MenuDto)
} else {
await menuService.create(values as MenuDto)
}
toast.push(
<Notification title="Başarılı" type="success">
{modalMode === 'edit' ? translate('::KayitGuncellendi') : translate('::KayitEklendi')}
</Notification>,
{ placement: 'top-end' },
)
setIsModalOpen(false)
refetch()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
{translate('::IslemBasarisiz')}
</Notification>,
{ placement: 'top-end' },
)
} finally {
setSubmitting(false)
}
}}
>
{({ values, isSubmitting }) => (
<Form>
<FormContainer>
<FormItem label="Code *" className="mb-2">
<Field
type="text"
name="code"
component={Input}
className="h-8 text-sm px-2"
autoFocus
/>
</FormItem>
<FormItem label="Display Name *" className="mb-2">
<Field
type="text"
name="displayName"
component={Input}
className="h-8 text-sm px-2"
/>
</FormItem>
<FormItem label="Order *" className="mb-2">
<Field
type="number"
name="order"
component={Input}
className="h-8 text-sm px-2"
/>
</FormItem>
<FormItem label="URL" className="mb-2">
<Field type="text" name="url" component={Input} className="h-8 text-sm px-2" />
</FormItem>
<FormItem label="Icon" className="mb-2">
<Field type="text" name="icon" component={Input} className="h-8 text-sm px-2" />
</FormItem>
<FormItem label="Parent Code" className="mb-2">
<Input
disabled
value={values.parentCode || ''}
className="h-8 text-sm px-2 bg-gray-100"
/>
</FormItem>
<FormItem label="CSS Class" className="mb-2">
<Field
type="text"
name="cssClass"
component={Input}
className="h-8 text-sm px-2"
/>
</FormItem>
<FormItem label="Permission Name" className="mb-2">
<Field
type="text"
autoComplete="off"
name="requiredPermissionName"
className="h-8 text-sm px-2"
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={permissions}
value={permissions?.filter(
(option) => option.value === values.requiredPermissionName,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem label="Target" className="mb-2">
<Field
type="text"
name="target"
component={Input}
className="h-8 text-sm px-2"
/>
</FormItem>
<FormItem label="Element Id" className="mb-2">
<Field
type="text"
name="elementId"
component={Input}
className="h-8 text-sm px-2"
/>
</FormItem>
<FormItem label="Is Disabled" className="mb-2">
<Field name="isDisabled" component={Switcher} />
</FormItem>
<div className="flex justify-end gap-2 mt-4">
<Button variant="plain" size="sm" onClick={() => setIsModalOpen(false)}>
{translate('::Cancel')}
</Button>
<Button type="submit" variant="solid" size="sm" loading={isSubmitting}>
{modalMode === 'edit' ? translate('::Save') : translate('::Create')}
</Button>
</div>
</FormContainer>
</Form>
)}
</Formik>
</Dialog>
)}
</div>
)
}