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 {
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,21 @@ import {
|
||||||
FaEdit,
|
FaEdit,
|
||||||
FaTimes,
|
FaTimes,
|
||||||
FaArrowRight,
|
FaArrowRight,
|
||||||
|
FaChevronDown,
|
||||||
|
FaChevronRight,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||||
import { MenuService } from '@/services/menu.service'
|
import { getMenus } from '@/services/menu.service'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { CascadeBehavior, SqlTableRelation, RelationshipType } from '@/proxy/developerKit/models'
|
import { CascadeBehavior, SqlTableRelation, RelationshipType } from '@/proxy/developerKit/models'
|
||||||
|
import navigationIcon from '@/proxy/menus/navigation-icon.config'
|
||||||
|
import { MenuItem } from '@/proxy/menus/menu'
|
||||||
|
import {
|
||||||
|
MenuTreeNode,
|
||||||
|
buildMenuTree,
|
||||||
|
filterNonLinkNodes,
|
||||||
|
} from '@/views/admin/listForm/WizardStep1'
|
||||||
|
import { MenuAddDialog } from '../shared/MenuAddDialog'
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -44,11 +54,6 @@ interface ColumnDefinition {
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuOption {
|
|
||||||
value: string
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableSettings {
|
interface TableSettings {
|
||||||
menuValue: string
|
menuValue: string
|
||||||
menuPrefix: string
|
menuPrefix: string
|
||||||
|
|
@ -234,13 +239,27 @@ function generateCreateTableSql(
|
||||||
for (const rel of relationships) {
|
for (const rel of relationships) {
|
||||||
if (!rel.fkColumnName.trim() || !rel.referencedTable.trim()) continue
|
if (!rel.fkColumnName.trim() || !rel.referencedTable.trim()) continue
|
||||||
const constraintName = `FK_${tableName}_${rel.fkColumnName}`
|
const constraintName = `FK_${tableName}_${rel.fkColumnName}`
|
||||||
const cascadeDelete = rel.cascadeDelete === 'NoAction' ? 'NO ACTION' : rel.cascadeDelete.replace(/([A-Z])/g, ' $1').trim().toUpperCase()
|
const cascadeDelete =
|
||||||
const cascadeUpdate = rel.cascadeUpdate === 'NoAction' ? 'NO ACTION' : rel.cascadeUpdate.replace(/([A-Z])/g, ' $1').trim().toUpperCase()
|
rel.cascadeDelete === 'NoAction'
|
||||||
|
? 'NO ACTION'
|
||||||
|
: rel.cascadeDelete
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
const cascadeUpdate =
|
||||||
|
rel.cascadeUpdate === 'NoAction'
|
||||||
|
? 'NO ACTION'
|
||||||
|
: rel.cascadeUpdate
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
fkLines.push('')
|
fkLines.push('')
|
||||||
fkLines.push(`ALTER TABLE ${fullTableName}`)
|
fkLines.push(`ALTER TABLE ${fullTableName}`)
|
||||||
fkLines.push(` ADD CONSTRAINT [${constraintName}]`)
|
fkLines.push(` ADD CONSTRAINT [${constraintName}]`)
|
||||||
fkLines.push(` FOREIGN KEY ([${rel.fkColumnName}])`)
|
fkLines.push(` FOREIGN KEY ([${rel.fkColumnName}])`)
|
||||||
fkLines.push(` REFERENCES [dbo].[${rel.referencedTable}] ([${rel.referencedColumn || 'Id'}])`)
|
fkLines.push(
|
||||||
|
` REFERENCES [dbo].[${rel.referencedTable}] ([${rel.referencedColumn || 'Id'}])`,
|
||||||
|
)
|
||||||
fkLines.push(` ON DELETE ${cascadeDelete}`)
|
fkLines.push(` ON DELETE ${cascadeDelete}`)
|
||||||
fkLines.push(` ON UPDATE ${cascadeUpdate};`)
|
fkLines.push(` ON UPDATE ${cascadeUpdate};`)
|
||||||
}
|
}
|
||||||
|
|
@ -359,7 +378,8 @@ function generateAlterTableSql(
|
||||||
const orig = origById.get(col.id)
|
const orig = origById.get(col.id)
|
||||||
if (!orig) return // new column, already handled above
|
if (!orig) return // new column, already handled above
|
||||||
|
|
||||||
const nameChanged = orig.columnName.trim().toLowerCase() !== col.columnName.trim().toLowerCase()
|
const nameChanged =
|
||||||
|
orig.columnName.trim().toLowerCase() !== col.columnName.trim().toLowerCase()
|
||||||
const typeChanged = orig.dataType !== col.dataType || orig.maxLength !== col.maxLength
|
const typeChanged = orig.dataType !== col.dataType || orig.maxLength !== col.maxLength
|
||||||
const nullChanged = orig.isNullable !== col.isNullable
|
const nullChanged = orig.isNullable !== col.isNullable
|
||||||
|
|
||||||
|
|
@ -390,7 +410,12 @@ function generateAlterTableSql(
|
||||||
|
|
||||||
// 🔗 FK Diff: drop removed / drop+re-add modified / add new
|
// 🔗 FK Diff: drop removed / drop+re-add modified / add new
|
||||||
const fkCascadeSql = (b: CascadeBehavior) =>
|
const fkCascadeSql = (b: CascadeBehavior) =>
|
||||||
b === 'NoAction' ? 'NO ACTION' : b.replace(/([A-Z])/g, ' $1').trim().toUpperCase()
|
b === 'NoAction'
|
||||||
|
? 'NO ACTION'
|
||||||
|
: b
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
|
||||||
const addFkSql = (rel: SqlTableRelation) => {
|
const addFkSql = (rel: SqlTableRelation) => {
|
||||||
const cname = rel.constraintName ?? `FK_${tableName}_${rel.fkColumnName}`
|
const cname = rel.constraintName ?? `FK_${tableName}_${rel.fkColumnName}`
|
||||||
|
|
@ -461,6 +486,138 @@ function generateAlterTableSql(
|
||||||
const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'T-SQL Önizleme'] as const
|
const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'T-SQL Önizleme'] as const
|
||||||
type Step = 0 | 1 | 2 | 3
|
type Step = 0 | 1 | 2 | 3
|
||||||
|
|
||||||
|
// ─── Simple Menu Tree (read-only selection) ───────────────────────────────────
|
||||||
|
|
||||||
|
interface SimpleMenuTreeNodeProps {
|
||||||
|
node: MenuTreeNode & { shortName?: string }
|
||||||
|
depth: number
|
||||||
|
selectedCode: string
|
||||||
|
onSelect: (code: string) => void
|
||||||
|
expanded: Set<string>
|
||||||
|
onToggle: (code: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function SimpleMenuTreeNode({
|
||||||
|
node,
|
||||||
|
depth,
|
||||||
|
selectedCode,
|
||||||
|
onSelect,
|
||||||
|
expanded,
|
||||||
|
onToggle,
|
||||||
|
}: SimpleMenuTreeNodeProps) {
|
||||||
|
const hasChildren = node.children.length > 0
|
||||||
|
const isExpanded = expanded.has(node.code)
|
||||||
|
const isSelected = node.code === selectedCode
|
||||||
|
const NodeIcon = node.icon ? navigationIcon[node.icon] : null
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1 px-2 py-1.5 rounded mx-1 cursor-pointer ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-indigo-500 text-white'
|
||||||
|
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200'
|
||||||
|
}`}
|
||||||
|
style={{ paddingLeft: `${8 + depth * 16}px` }}
|
||||||
|
onClick={() => onSelect(node.code)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{hasChildren ? (
|
||||||
|
isExpanded ? (
|
||||||
|
<FaChevronDown className="text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<FaChevronRight className="text-gray-400" />
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
{NodeIcon && (
|
||||||
|
<NodeIcon
|
||||||
|
className={`text-base shrink-0 ${isSelected ? 'text-indigo-200' : 'text-gray-400'}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="flex-1 text-sm truncate">{translate('::' + node.code)}</span>
|
||||||
|
{node.shortName && (
|
||||||
|
<span
|
||||||
|
className={`text-xs shrink-0 font-mono px-1.5 py-0.5 rounded ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-indigo-400 text-indigo-100'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{node.shortName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isExpanded &&
|
||||||
|
node.children.map((child: MenuTreeNode) => (
|
||||||
|
<SimpleMenuTreeNode
|
||||||
|
key={child.code}
|
||||||
|
node={child as MenuTreeNode & { shortName?: string }}
|
||||||
|
depth={depth + 1}
|
||||||
|
selectedCode={selectedCode}
|
||||||
|
onSelect={onSelect}
|
||||||
|
expanded={expanded}
|
||||||
|
onToggle={onToggle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SimpleMenuTreeSelectProps {
|
||||||
|
selectedCode: string
|
||||||
|
onSelect: (code: string) => void
|
||||||
|
nodes: MenuTreeNode[]
|
||||||
|
isLoading: boolean
|
||||||
|
invalid?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function SimpleMenuTreeSelect({
|
||||||
|
selectedCode,
|
||||||
|
onSelect,
|
||||||
|
nodes,
|
||||||
|
isLoading,
|
||||||
|
invalid,
|
||||||
|
}: SimpleMenuTreeSelectProps) {
|
||||||
|
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
|
const toggle = (code: string) =>
|
||||||
|
setExpanded((prev) => {
|
||||||
|
const n = new Set(prev)
|
||||||
|
n.has(code) ? n.delete(code) : n.add(code)
|
||||||
|
return n
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`rounded-lg border ${
|
||||||
|
invalid ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||||
|
} bg-white dark:bg-gray-800 overflow-hidden`}
|
||||||
|
>
|
||||||
|
<div className="h-56 overflow-y-auto py-1">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="px-4 py-3 text-sm text-gray-400">Loading…</div>
|
||||||
|
) : nodes.length === 0 ? (
|
||||||
|
<div className="px-4 py-3 text-sm text-gray-400">No menus available</div>
|
||||||
|
) : (
|
||||||
|
nodes.map((node) => (
|
||||||
|
<SimpleMenuTreeNode
|
||||||
|
key={node.code}
|
||||||
|
node={node as MenuTreeNode & { shortName?: string }}
|
||||||
|
depth={0}
|
||||||
|
selectedCode={selectedCode}
|
||||||
|
onSelect={onSelect}
|
||||||
|
expanded={expanded}
|
||||||
|
onToggle={toggle}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const createEmptyColumn = (): ColumnDefinition => ({
|
const createEmptyColumn = (): ColumnDefinition => ({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
columnName: '',
|
columnName: '',
|
||||||
|
|
@ -497,7 +654,10 @@ const SqlTableDesignerDialog = ({
|
||||||
const [originalColumns, setOriginalColumns] = useState<ColumnDefinition[]>([])
|
const [originalColumns, setOriginalColumns] = useState<ColumnDefinition[]>([])
|
||||||
const [colsLoading, setColsLoading] = useState(false)
|
const [colsLoading, setColsLoading] = useState(false)
|
||||||
const [settings, setSettings] = useState<TableSettings>(DEFAULT_SETTINGS)
|
const [settings, setSettings] = useState<TableSettings>(DEFAULT_SETTINGS)
|
||||||
const [menuOptions, setMenuOptions] = useState<MenuOption[]>([])
|
const [rawMenuItems, setRawMenuItems] = useState<MenuItem[]>([])
|
||||||
|
const [menuTree, setMenuTree] = useState<MenuTreeNode[]>([])
|
||||||
|
const [selectedMenuCode, setSelectedMenuCode] = useState('')
|
||||||
|
const [menuAddDialogOpen, setMenuAddDialogOpen] = useState(false)
|
||||||
const [menuLoading, setMenuLoading] = useState(false)
|
const [menuLoading, setMenuLoading] = useState(false)
|
||||||
const [relationships, setRelationships] = useState<SqlTableRelation[]>([])
|
const [relationships, setRelationships] = useState<SqlTableRelation[]>([])
|
||||||
const [originalRelationships, setOriginalRelationships] = useState<SqlTableRelation[]>([])
|
const [originalRelationships, setOriginalRelationships] = useState<SqlTableRelation[]>([])
|
||||||
|
|
@ -509,21 +669,32 @@ const SqlTableDesignerDialog = ({
|
||||||
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
|
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
|
||||||
const [targetColsLoading, setTargetColsLoading] = useState(false)
|
const [targetColsLoading, setTargetColsLoading] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
const reloadMenus = (onLoaded?: (items: MenuItem[]) => void) => {
|
||||||
if (!isOpen) return
|
|
||||||
setMenuLoading(true)
|
setMenuLoading(true)
|
||||||
const svc = new MenuService()
|
getMenus(0, 1000)
|
||||||
svc
|
|
||||||
.getListMainMenu()
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const opts: MenuOption[] = (res.data || []).map((m: any) => ({
|
const items = (res.data?.items ?? []) as MenuItem[]
|
||||||
value: m.shortName,
|
const filtered = items.filter((m) => !!m.shortName?.trim())
|
||||||
label: m.displayName,
|
setRawMenuItems(filtered)
|
||||||
}))
|
const tree = filterNonLinkNodes(buildMenuTree(filtered))
|
||||||
setMenuOptions(opts)
|
setMenuTree(tree)
|
||||||
|
onLoaded?.(filtered)
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => setMenuLoading(false))
|
.finally(() => setMenuLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return
|
||||||
|
reloadMenus((items) => {
|
||||||
|
// In edit mode, auto-select the matching menu code by shortName
|
||||||
|
if (initialTableData) {
|
||||||
|
const parts = initialTableData.tableName.split('_')
|
||||||
|
const derivedShortName = parts[0] ?? ''
|
||||||
|
const match = items.find((m) => m.shortName === derivedShortName)
|
||||||
|
if (match?.code) setSelectedMenuCode(match.code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (dataSource) {
|
if (dataSource) {
|
||||||
sqlObjectManagerService
|
sqlObjectManagerService
|
||||||
|
|
@ -618,7 +789,15 @@ const SqlTableDesignerDialog = ({
|
||||||
originalRelationships,
|
originalRelationships,
|
||||||
)
|
)
|
||||||
: generateCreateTableSql(columns, settings, relationships),
|
: generateCreateTableSql(columns, settings, relationships),
|
||||||
[isEditMode, originalColumns, columns, settings, initialTableData, relationships, originalRelationships],
|
[
|
||||||
|
isEditMode,
|
||||||
|
originalColumns,
|
||||||
|
columns,
|
||||||
|
settings,
|
||||||
|
initialTableData,
|
||||||
|
relationships,
|
||||||
|
originalRelationships,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Column operations ──────────────────────────────────────────────────────
|
// ── Column operations ──────────────────────────────────────────────────────
|
||||||
|
|
@ -629,9 +808,7 @@ const SqlTableDesignerDialog = ({
|
||||||
|
|
||||||
const addFullAuditedColumns = () => {
|
const addFullAuditedColumns = () => {
|
||||||
const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase()))
|
const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase()))
|
||||||
const toAdd = FULL_AUDIT_COLUMNS.filter(
|
const toAdd = FULL_AUDIT_COLUMNS.filter((c) => !existingNames.has(c.columnName.toLowerCase()))
|
||||||
(c) => !existingNames.has(c.columnName.toLowerCase()),
|
|
||||||
)
|
|
||||||
setColumns((prev) => {
|
setColumns((prev) => {
|
||||||
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
|
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
|
||||||
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
|
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
|
||||||
|
|
@ -674,12 +851,14 @@ const SqlTableDesignerDialog = ({
|
||||||
const buildTableName = (prefix: string, entity: string) =>
|
const buildTableName = (prefix: string, entity: string) =>
|
||||||
prefix && entity ? `${prefix}_D_${entity}` : ''
|
prefix && entity ? `${prefix}_D_${entity}` : ''
|
||||||
|
|
||||||
const onMenuChange = (value: string) => {
|
const onMenuCodeSelect = (code: string) => {
|
||||||
const opt = menuOptions.find((o) => o.value === value)
|
if (isEditMode) return
|
||||||
const prefix = opt?.value ?? ''
|
const item = rawMenuItems.find((m) => m.code === code)
|
||||||
|
const prefix = item?.shortName ?? ''
|
||||||
|
setSelectedMenuCode(code)
|
||||||
setSettings((s) => ({
|
setSettings((s) => ({
|
||||||
...s,
|
...s,
|
||||||
menuValue: value,
|
menuValue: prefix,
|
||||||
menuPrefix: prefix,
|
menuPrefix: prefix,
|
||||||
tableName: buildTableName(prefix, s.entityName),
|
tableName: buildTableName(prefix, s.entityName),
|
||||||
}))
|
}))
|
||||||
|
|
@ -813,7 +992,8 @@ const SqlTableDesignerDialog = ({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" title={translate('::App.SqlQueryManager.Error')}>
|
<Notification type="danger" title={translate('::App.SqlQueryManager.Error')}>
|
||||||
{error.response?.data?.error?.message || translate('::App.SqlQueryManager.TableDeployFailed')}
|
{error.response?.data?.error?.message ||
|
||||||
|
translate('::App.SqlQueryManager.TableDeployFailed')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'top-center' },
|
{ placement: 'top-center' },
|
||||||
)
|
)
|
||||||
|
|
@ -831,6 +1011,8 @@ const SqlTableDesignerDialog = ({
|
||||||
setOriginalRelationships([])
|
setOriginalRelationships([])
|
||||||
setDbTables([])
|
setDbTables([])
|
||||||
setTargetTableColumns([])
|
setTargetTableColumns([])
|
||||||
|
setSelectedMenuCode('')
|
||||||
|
setMenuAddDialogOpen(false)
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -878,7 +1060,8 @@ const SqlTableDesignerDialog = ({
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{colsLoading && (
|
{colsLoading && (
|
||||||
<div className="flex items-center justify-center py-12 text-gray-500">
|
<div className="flex items-center justify-center py-12 text-gray-500">
|
||||||
<span className="animate-spin mr-2">◠</span> {translate('::App.SqlQueryManager.LoadingColumns')}
|
<span className="animate-spin mr-2">◠</span>{' '}
|
||||||
|
{translate('::App.SqlQueryManager.LoadingColumns')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!colsLoading && (
|
{!colsLoading && (
|
||||||
|
|
@ -893,10 +1076,22 @@ const SqlTableDesignerDialog = ({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button size="xs" variant="solid" color="red-600" icon={<FaTrash />} onClick={clearAllColumns}>
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="solid"
|
||||||
|
color="red-600"
|
||||||
|
icon={<FaTrash />}
|
||||||
|
onClick={clearAllColumns}
|
||||||
|
>
|
||||||
{translate('::App.SqlQueryManager.ClearAllColumns')}
|
{translate('::App.SqlQueryManager.ClearAllColumns')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="xs" variant="solid" color="blue-600" icon={<FaPlus />} onClick={addColumn}>
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
icon={<FaPlus />}
|
||||||
|
onClick={addColumn}
|
||||||
|
>
|
||||||
{translate('::App.SqlQueryManager.AddColumn')}
|
{translate('::App.SqlQueryManager.AddColumn')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -907,10 +1102,14 @@ const SqlTableDesignerDialog = ({
|
||||||
<div className="col-span-3">{translate('::App.SqlQueryManager.ColumnName')}</div>
|
<div className="col-span-3">{translate('::App.SqlQueryManager.ColumnName')}</div>
|
||||||
<div className="col-span-3">{translate('::App.SqlQueryManager.DataType')}</div>
|
<div className="col-span-3">{translate('::App.SqlQueryManager.DataType')}</div>
|
||||||
<div className="col-span-1 text-center">{translate('::App.SqlQueryManager.Max')}</div>
|
<div className="col-span-1 text-center">{translate('::App.SqlQueryManager.Max')}</div>
|
||||||
<div className="col-span-1 text-center">{translate('::App.SqlQueryManager.Nullable')}</div>
|
<div className="col-span-1 text-center">
|
||||||
|
{translate('::App.SqlQueryManager.Nullable')}
|
||||||
|
</div>
|
||||||
<div className="col-span-2">{translate('::App.SqlQueryManager.DefaultValue')}</div>
|
<div className="col-span-2">{translate('::App.SqlQueryManager.DefaultValue')}</div>
|
||||||
<div className="col-span-1">{translate('::App.SqlQueryManager.Note')}</div>
|
<div className="col-span-1">{translate('::App.SqlQueryManager.Note')}</div>
|
||||||
<div className="col-span-1 text-center">{translate('::App.SqlQueryManager.Actions')}</div>
|
<div className="col-span-1 text-center">
|
||||||
|
{translate('::App.SqlQueryManager.Actions')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Editable column rows */}
|
{/* Editable column rows */}
|
||||||
|
|
@ -930,9 +1129,7 @@ const SqlTableDesignerDialog = ({
|
||||||
const isDuplicate =
|
const isDuplicate =
|
||||||
col.columnName.trim() !== '' &&
|
col.columnName.trim() !== '' &&
|
||||||
duplicateColumnNames.has(col.columnName.trim().toLowerCase())
|
duplicateColumnNames.has(col.columnName.trim().toLowerCase())
|
||||||
const isNewRow =
|
const isNewRow = isEditMode && !originalColumns.some((o) => o.id === col.id)
|
||||||
isEditMode &&
|
|
||||||
!originalColumns.some((o) => o.id === col.id)
|
|
||||||
|
|
||||||
const origRow = isEditMode ? originalColumns.find((o) => o.id === col.id) : undefined
|
const origRow = isEditMode ? originalColumns.find((o) => o.id === col.id) : undefined
|
||||||
const isRenamedRow =
|
const isRenamedRow =
|
||||||
|
|
@ -1047,7 +1244,8 @@ const SqlTableDesignerDialog = ({
|
||||||
<button
|
<button
|
||||||
onClick={() => removeColumn(col.id)}
|
onClick={() => removeColumn(col.id)}
|
||||||
className="p-1 rounded hover:bg-red-50 dark:hover:bg-red-900/30 text-red-500"
|
className="p-1 rounded hover:bg-red-50 dark:hover:bg-red-900/30 text-red-500"
|
||||||
title={translate('::App.SqlQueryManager.Delete')}>
|
title={translate('::App.SqlQueryManager.Delete')}
|
||||||
|
>
|
||||||
<FaTrash className="text-xs" />
|
<FaTrash className="text-xs" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1057,8 +1255,7 @@ const SqlTableDesignerDialog = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Id warning */}
|
{/* Id warning */}
|
||||||
{!isEditMode &&
|
{!isEditMode && !columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
|
||||||
!columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
|
|
||||||
<div className="px-2 py-1.5 mt-2 bg-orange-50 dark:bg-orange-900/20 border border-orange-300 dark:border-orange-700 rounded text-xs text-orange-700 dark:text-orange-300">
|
<div className="px-2 py-1.5 mt-2 bg-orange-50 dark:bg-orange-900/20 border border-orange-300 dark:border-orange-700 rounded text-xs text-orange-700 dark:text-orange-300">
|
||||||
{translate('::App.SqlQueryManager.NoIdColumnWarning')}
|
{translate('::App.SqlQueryManager.NoIdColumnWarning')}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1075,23 +1272,57 @@ const SqlTableDesignerDialog = ({
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{/* Menu Name */}
|
{/* Menu Name */}
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<label className="block text-sm font-medium mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<label className="block text-sm font-medium">
|
||||||
{translate('::App.SqlQueryManager.MenuName')} <span className="text-red-500">*</span>
|
{translate('::App.SqlQueryManager.MenuName')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<div className="flex items-center gap-2">
|
||||||
className="w-full px-3 py-2 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
<button
|
||||||
value={settings.menuValue}
|
type="button"
|
||||||
disabled={menuLoading || isEditMode} // Menu cannot be changed in edit mode (as it determines the table name)
|
disabled={isEditMode}
|
||||||
onChange={(e) => onMenuChange(e.target.value)}
|
onClick={() => setMenuAddDialogOpen(true)}
|
||||||
|
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-green-500 text-white hover:bg-green-600 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<option value=""></option>
|
<FaPlus className="text-xs" />{' '}
|
||||||
{menuOptions.map((opt) => (
|
{translate('::ListForms.Wizard.Step1.AddNewMenu') || 'Add Menu'}
|
||||||
<option key={opt.value} value={opt.value}>
|
</button>
|
||||||
{opt.label} ({opt.value})
|
{settings.menuValue && !isEditMode && (
|
||||||
</option>
|
<button
|
||||||
))}
|
type="button"
|
||||||
</select>
|
onClick={() => {
|
||||||
{menuLoading && <p className="text-xs text-gray-400 mt-1">{translate('::App.SqlQueryManager.Loading')}</p>}
|
setSelectedMenuCode('')
|
||||||
|
setSettings((s) => ({ ...s, menuValue: '', menuPrefix: '', tableName: '' }))
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
|
||||||
|
>
|
||||||
|
<FaTimes className="text-xs" />{' '}
|
||||||
|
{translate('::ListForms.Wizard.ClearSelection') || 'Clear'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SimpleMenuTreeSelect
|
||||||
|
selectedCode={selectedMenuCode}
|
||||||
|
onSelect={isEditMode ? () => {} : onMenuCodeSelect}
|
||||||
|
nodes={menuTree}
|
||||||
|
isLoading={menuLoading}
|
||||||
|
invalid={!settings.menuValue && !isEditMode && !menuLoading}
|
||||||
|
/>
|
||||||
|
{settings.menuValue && (
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
<span className="font-mono font-semibold text-indigo-600 dark:text-indigo-400">
|
||||||
|
{settings.menuValue}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<MenuAddDialog
|
||||||
|
isOpen={menuAddDialogOpen}
|
||||||
|
onClose={() => setMenuAddDialogOpen(false)}
|
||||||
|
initialParentCode={selectedMenuCode}
|
||||||
|
initialOrder={999}
|
||||||
|
rawItems={rawMenuItems}
|
||||||
|
onSaved={() => reloadMenus()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Entity Name */}
|
{/* Entity Name */}
|
||||||
|
|
@ -1111,7 +1342,9 @@ const SqlTableDesignerDialog = ({
|
||||||
|
|
||||||
{/* Table Name (readonly, auto-generated) */}
|
{/* Table Name (readonly, auto-generated) */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">{translate('::App.SqlQueryManager.TableName')}</label>
|
<label className="block text-sm font-medium mb-1">
|
||||||
|
{translate('::App.SqlQueryManager.TableName')}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
readOnly
|
readOnly
|
||||||
|
|
@ -1122,10 +1355,8 @@ const SqlTableDesignerDialog = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Warning: no Id column */}
|
{/* Warning: no Id column */}
|
||||||
{!isEditMode &&
|
{!isEditMode && !columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
|
||||||
!columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
|
|
||||||
<div className="px-3 py-2 bg-red-50 dark:bg-red-900/20 border border-red-300 dark:border-red-700 rounded text-xs text-red-700 dark:text-red-300">
|
<div className="px-3 py-2 bg-red-50 dark:bg-red-900/20 border border-red-300 dark:border-red-700 rounded text-xs text-red-700 dark:text-red-300">
|
||||||
{translate('::App.SqlQueryManager.NoIdColumnError')}
|
{translate('::App.SqlQueryManager.NoIdColumnError')}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1140,10 +1371,12 @@ const SqlTableDesignerDialog = ({
|
||||||
{/* Loading indicator for edit mode */}
|
{/* Loading indicator for edit mode */}
|
||||||
{fksLoading && (
|
{fksLoading && (
|
||||||
<div className="flex items-center justify-center py-10 text-gray-500 text-sm gap-2">
|
<div className="flex items-center justify-center py-10 text-gray-500 text-sm gap-2">
|
||||||
<span className="animate-spin">◠</span> {translate('::App.SqlQueryManager.LoadingFkConstraints')}
|
<span className="animate-spin">◠</span>{' '}
|
||||||
|
{translate('::App.SqlQueryManager.LoadingFkConstraints')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!fksLoading && <>
|
{!fksLoading && (
|
||||||
|
<>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
|
|
@ -1155,7 +1388,8 @@ const SqlTableDesignerDialog = ({
|
||||||
onClick={openAddFk}
|
onClick={openAddFk}
|
||||||
className="flex items-center gap-1 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white text-xs rounded-lg transition-colors"
|
className="flex items-center gap-1 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white text-xs rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<FaPlus className="w-2.5 h-2.5" /> {translate('::App.SqlQueryManager.AddRelationship')}
|
<FaPlus className="w-2.5 h-2.5" />{' '}
|
||||||
|
{translate('::App.SqlQueryManager.AddRelationship')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1163,8 +1397,12 @@ const SqlTableDesignerDialog = ({
|
||||||
{relationships.length === 0 && (
|
{relationships.length === 0 && (
|
||||||
<div className="py-8 text-center border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-xl bg-gray-50 dark:bg-gray-800/50">
|
<div className="py-8 text-center border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-xl bg-gray-50 dark:bg-gray-800/50">
|
||||||
<FaLink className="text-3xl mx-auto text-gray-300 dark:text-gray-600 mb-2" />
|
<FaLink className="text-3xl mx-auto text-gray-300 dark:text-gray-600 mb-2" />
|
||||||
<p className="text-sm text-gray-500">{translate('::App.SqlQueryManager.NoRelationshipsYet')}</p>
|
<p className="text-sm text-gray-500">
|
||||||
<p className="text-xs text-gray-400 mt-1">{translate('::App.SqlQueryManager.StepIsOptional')}</p>
|
{translate('::App.SqlQueryManager.NoRelationshipsYet')}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">
|
||||||
|
{translate('::App.SqlQueryManager.StepIsOptional')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1192,10 +1430,16 @@ const SqlTableDesignerDialog = ({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 mt-1 text-xs text-gray-500 flex-wrap">
|
<div className="flex items-center gap-3 mt-1 text-xs text-gray-500 flex-wrap">
|
||||||
<span>ON DELETE: <strong>{rel.cascadeDelete}</strong></span>
|
<span>
|
||||||
<span>ON UPDATE: <strong>{rel.cascadeUpdate}</strong></span>
|
ON DELETE: <strong>{rel.cascadeDelete}</strong>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
ON UPDATE: <strong>{rel.cascadeUpdate}</strong>
|
||||||
|
</span>
|
||||||
{rel.isRequired && (
|
{rel.isRequired && (
|
||||||
<span className="text-orange-600 dark:text-orange-400">{translate('::App.SqlQueryManager.Required')}</span>
|
<span className="text-orange-600 dark:text-orange-400">
|
||||||
|
{translate('::App.SqlQueryManager.Required')}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
{rel.description && (
|
{rel.description && (
|
||||||
<span className="text-gray-400 italic">{rel.description}</span>
|
<span className="text-gray-400 italic">{rel.description}</span>
|
||||||
|
|
@ -1221,16 +1465,20 @@ const SqlTableDesignerDialog = ({
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>}
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* FK Add/Edit Modal */}
|
{/* FK Add/Edit Modal */}
|
||||||
{fkModalOpen && createPortal(
|
{fkModalOpen &&
|
||||||
|
createPortal(
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-lg flex flex-col">
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-lg flex flex-col">
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<h2 className="text-base font-bold text-gray-900 dark:text-white">
|
<h2 className="text-base font-bold text-gray-900 dark:text-white">
|
||||||
{editingFkId ? translate('::App.SqlQueryManager.EditRelationship') : translate('::App.SqlQueryManager.AddNewRelationship')}
|
{editingFkId
|
||||||
|
? translate('::App.SqlQueryManager.EditRelationship')
|
||||||
|
: translate('::App.SqlQueryManager.AddNewRelationship')}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => setFkModalOpen(false)}
|
onClick={() => setFkModalOpen(false)}
|
||||||
|
|
@ -1277,7 +1525,9 @@ const SqlTableDesignerDialog = ({
|
||||||
onChange={(e) => setFkForm((f) => ({ ...f, fkColumnName: e.target.value }))}
|
onChange={(e) => setFkForm((f) => ({ ...f, fkColumnName: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
<option value="">{translate('::App.SqlQueryManager.SelectPlaceholder')}</option>
|
<option value="">
|
||||||
|
{translate('::App.SqlQueryManager.SelectPlaceholder')}
|
||||||
|
</option>
|
||||||
{columns
|
{columns
|
||||||
.filter((c) => c.columnName.trim())
|
.filter((c) => c.columnName.trim())
|
||||||
.map((c) => (
|
.map((c) => (
|
||||||
|
|
@ -1300,7 +1550,9 @@ const SqlTableDesignerDialog = ({
|
||||||
}}
|
}}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
<option value="">{translate('::App.SqlQueryManager.SelectPlaceholder')}</option>
|
<option value="">
|
||||||
|
{translate('::App.SqlQueryManager.SelectPlaceholder')}
|
||||||
|
</option>
|
||||||
{dbTables.map((t) => (
|
{dbTables.map((t) => (
|
||||||
<option key={`${t.schemaName}.${t.tableName}`} value={t.tableName}>
|
<option key={`${t.schemaName}.${t.tableName}`} value={t.tableName}>
|
||||||
{t.tableName}
|
{t.tableName}
|
||||||
|
|
@ -1312,7 +1564,8 @@ const SqlTableDesignerDialog = ({
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
||||||
{translate('::App.SqlQueryManager.TargetColumn')}{targetColsLoading ? ` — ${translate('::App.SqlQueryManager.Loading')}` : ''}
|
{translate('::App.SqlQueryManager.TargetColumn')}
|
||||||
|
{targetColsLoading ? ` — ${translate('::App.SqlQueryManager.Loading')}` : ''}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={fkForm.referencedColumn}
|
value={fkForm.referencedColumn}
|
||||||
|
|
@ -1320,7 +1573,9 @@ const SqlTableDesignerDialog = ({
|
||||||
disabled={targetColsLoading || targetTableColumns.length === 0}
|
disabled={targetColsLoading || targetTableColumns.length === 0}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500 disabled:opacity-60"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500 disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<option value="">{translate('::App.SqlQueryManager.SelectTargetTableFirst')}</option>
|
<option value="">
|
||||||
|
{translate('::App.SqlQueryManager.SelectTargetTableFirst')}
|
||||||
|
</option>
|
||||||
{targetTableColumns.map((col) => (
|
{targetTableColumns.map((col) => (
|
||||||
<option key={col} value={col}>
|
<option key={col} value={col}>
|
||||||
{col}
|
{col}
|
||||||
|
|
@ -1338,12 +1593,17 @@ const SqlTableDesignerDialog = ({
|
||||||
<select
|
<select
|
||||||
value={fkForm.cascadeUpdate}
|
value={fkForm.cascadeUpdate}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFkForm((f) => ({ ...f, cascadeUpdate: e.target.value as CascadeBehavior }))
|
setFkForm((f) => ({
|
||||||
|
...f,
|
||||||
|
cascadeUpdate: e.target.value as CascadeBehavior,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
{CASCADE_OPTIONS.map((o) => (
|
{CASCADE_OPTIONS.map((o) => (
|
||||||
<option key={o.value} value={o.value}>{o.label}</option>
|
<option key={o.value} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1354,12 +1614,17 @@ const SqlTableDesignerDialog = ({
|
||||||
<select
|
<select
|
||||||
value={fkForm.cascadeDelete}
|
value={fkForm.cascadeDelete}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFkForm((f) => ({ ...f, cascadeDelete: e.target.value as CascadeBehavior }))
|
setFkForm((f) => ({
|
||||||
|
...f,
|
||||||
|
cascadeDelete: e.target.value as CascadeBehavior,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
{CASCADE_OPTIONS.map((o) => (
|
{CASCADE_OPTIONS.map((o) => (
|
||||||
<option key={o.value} value={o.value}>{o.label}</option>
|
<option key={o.value} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1374,7 +1639,9 @@ const SqlTableDesignerDialog = ({
|
||||||
onChange={(e) => setFkForm((f) => ({ ...f, isRequired: e.target.checked }))}
|
onChange={(e) => setFkForm((f) => ({ ...f, isRequired: e.target.checked }))}
|
||||||
className="w-4 h-4 text-indigo-600 rounded"
|
className="w-4 h-4 text-indigo-600 rounded"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">{translate('::App.SqlQueryManager.Required')}</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{translate('::App.SqlQueryManager.Required')}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1411,7 +1678,7 @@ const SqlTableDesignerDialog = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -1492,7 +1759,11 @@ const SqlTableDesignerDialog = ({
|
||||||
icon={<FaCloudUploadAlt />}
|
icon={<FaCloudUploadAlt />}
|
||||||
onClick={handleDeploy}
|
onClick={handleDeploy}
|
||||||
loading={isDeploying}
|
loading={isDeploying}
|
||||||
disabled={!dataSource || isDeploying || (isEditMode && generatedSql.includes('Henüz değişiklik yapılmadı'))}
|
disabled={
|
||||||
|
!dataSource ||
|
||||||
|
isDeploying ||
|
||||||
|
(isEditMode && generatedSql.includes('Henüz değişiklik yapılmadı'))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{translate('::App.SqlQueryManager.Deploy')}
|
{translate('::App.SqlQueryManager.Deploy')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
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