- {[
- { label: translate('::App.Listform.ListformField.Group'), value: groups.length },
- { label: translate('::App.Platform.Field'), value: totalFields },
- {
- label: translate('::App.WizardStep4.EditingForm'),
- value: editingFormFields.length,
- },
- {
- label: translate('::App.Listform.ListformField.Column'),
- value: selectedColumns.size,
- },
- {
- label: translate('::App.ListFormEdit.SubForms'),
- value: subForms.length,
- },
- {
- label: translate('::App.ListFormEdit.TabWidgets'),
- value: widgets.length,
- },
- {
- label: translate('::App.ListFormEdit.TabWorkflow'),
- value: workflowItems.length,
- },
- {
- label: translate('::App.Platform.TabTodo'),
- value: values.todo ? 1 : 0,
- },
- ].map((s) => (
-
-
- {s.value}
+ {/* Stats — Custom yolunda sayilacak ListForm artefakti yok. */}
+ {!isCustom && (
+
+ {[
+ { label: translate('::App.Listform.ListformField.Group'), value: groups.length },
+ { label: translate('::App.Platform.Field'), value: totalFields },
+ {
+ label: translate('::App.WizardStep4.EditingForm'),
+ value: editingFormFields.length,
+ },
+ {
+ label: translate('::App.Listform.ListformField.Column'),
+ value: selectedColumns.size,
+ },
+ {
+ label: translate('::App.ListFormEdit.SubForms'),
+ value: subForms.length,
+ },
+ {
+ label: translate('::App.ListFormEdit.TabWidgets'),
+ value: widgets.length,
+ },
+ {
+ label: translate('::App.ListFormEdit.TabWorkflow'),
+ value: workflowItems.length,
+ },
+ {
+ label: translate('::App.Platform.TabTodo'),
+ value: values.todo ? 1 : 0,
+ },
+ ].map((s) => (
+
+
+ {s.value}
+
+
{s.label}
-
{s.label}
-
- ))}
-
+ ))}
+
+ )}
{/* Log panel — grows to fill remaining height */}
diff --git a/ui/src/views/admin/listForm/wizard/WizardStep2.tsx b/ui/src/views/admin/listForm/wizard/WizardStep2.tsx
index dec85eb5..51cf3279 100644
--- a/ui/src/views/admin/listForm/wizard/WizardStep2.tsx
+++ b/ui/src/views/admin/listForm/wizard/WizardStep2.tsx
@@ -501,7 +501,8 @@ const WizardStep2 = ({
-
+ {/* Menü URL'i Menu adımında gösterilir; burada yalnızca layout seçimi kalır. */}
+
void
+ translate: (key: string) => string
+ onNext: () => void
+}
+
+// ─── Kind card ────────────────────────────────────────────────────────────────
+
+interface KindCardProps {
+ active: boolean
+ icon: React.ReactNode
+ title: string
+ description: string
+ onSelect: () => void
+}
+
+function KindCard({ active, icon, title, description, onSelect }: KindCardProps) {
+ return (
+
+ )
+}
+
+// ─── WizardStepComponent ──────────────────────────────────────────────────────
+
+/**
+ * Wizard'ın yol ayrımı adımı: yalnızca tip seçimi yapılır.
+ * List seçilirse mevcut ListForm akışı aynen devam eder; Custom seçilirse bir
+ * sonraki adımda bileşen seçimi yapılır ve ListForm'a ait tüm adımlar devre dışı kalır.
+ */
+const WizardStepComponent = ({
+ values,
+ onKindChange,
+ translate,
+ onNext,
+}: WizardStepComponentProps) => {
+ const isCustom = values.componentKind === WizardComponentKindEnum.Custom
+
+ return (
+
+ {/* Bu adımda tek bir karar var; seçim ekranın ortasına yerleşir. */}
+
+
+
+
+ {translate('::App.WizardStepComponent.ComponentType')}
+
+
+ {translate('::App.WizardStepComponent.ComponentTypeHint')}
+
+
+
+
+ }
+ title={translate('::App.WizardStepComponent.ListComponent')}
+ description={translate('::App.WizardStepComponent.ListComponentHint')}
+ onSelect={() => onKindChange(WizardComponentKindEnum.List)}
+ />
+ }
+ title={translate('::App.WizardStepComponent.CustomComponent')}
+ description={translate('::App.WizardStepComponent.CustomComponentHint')}
+ onSelect={() => onKindChange(WizardComponentKindEnum.Custom)}
+ />
+
+
+
+
+ {/* ─── Fixed Footer ─────────────────────────────── */}
+
+ {/* Wizard'in ilk adimi oldugu icin geri butonu yok. */}
+
+ } onClick={onNext}>
+ {translate('::App.Platform.Next')}
+
+
+
+
+ )
+}
+
+export default WizardStepComponent
diff --git a/ui/src/views/admin/listForm/wizard/WizardStepCustomComponent.tsx b/ui/src/views/admin/listForm/wizard/WizardStepCustomComponent.tsx
new file mode 100644
index 00000000..3048df39
--- /dev/null
+++ b/ui/src/views/admin/listForm/wizard/WizardStepCustomComponent.tsx
@@ -0,0 +1,196 @@
+import { Button, FormItem, Select } from '@/components/ui'
+import { useComponents } from '@/contexts/ComponentContext'
+import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
+import { Field, FieldProps } from 'formik'
+import { useMemo } from 'react'
+import { FaArrowLeft, FaArrowRight, FaCube, FaExternalLinkAlt } from 'react-icons/fa'
+
+// ─── Props ────────────────────────────────────────────────────────────────────
+
+export interface WizardStepCustomComponentProps {
+ values: ListFormWizardDto
+ onCustomComponentChange: (name: string, routePath: string) => void
+ translate: (key: string) => string
+ onBack: () => void
+ onNext: () => void
+}
+
+// ─── WizardStepCustomComponent ────────────────────────────────────────────────
+
+/**
+ * Custom yolunda menüye bağlanacak bileşenin seçildiği adım. Seçimle birlikte
+ * bileşenin rota yolu ve canlı önizlemesi gösterilir; deploy sonrası menü ikonu
+ * tam olarak bu rotayı açar.
+ */
+const WizardStepCustomComponent = ({
+ values,
+ onCustomComponentChange,
+ translate,
+ onBack,
+ onNext,
+}: WizardStepCustomComponentProps) => {
+ const { components, compiling, componentErrors, renderComponent } = useComponents()
+
+ // Yalnızca aktif ve rota tanımlı bileşenler menüye bağlanabilir; pasif bir
+ // bileşenin rotası router'a hiç kaydedilmediği için menü boşa düşerdi.
+ const selectableComponents = useMemo(
+ () => components.filter((c) => c.isActive && c.routePath?.trim()),
+ [components],
+ )
+
+ const componentOptions = useMemo(
+ () =>
+ selectableComponents.map((c) => ({
+ value: c.name,
+ label: c.description?.trim() ? `${c.name} — ${c.description.trim()}` : c.name,
+ })),
+ [selectableComponents],
+ )
+
+ const selectedComponent = useMemo(
+ () => selectableComponents.find((c) => c.name === values.customComponentName),
+ [selectableComponents, values.customComponentName],
+ )
+
+ const compileError = values.customComponentName
+ ? componentErrors[values.customComponentName]
+ : undefined
+
+ const stepMissing = [
+ !values.customComponentName && translate('::App.WizardStepComponent.ComponentName'),
+ !!values.customComponentName &&
+ !values.menuUrl &&
+ translate('::App.WizardStepComponent.RoutePath'),
+ ].filter(Boolean) as string[]
+ const canGo = stepMissing.length === 0
+
+ return (
+
+
+
+
+ {({ field, form }: FieldProps) => (
+
+
+
+ {/* Deploy sonrası menüye eklenen ikon bu rotayı açar. */}
+
+ {translate('::App.WizardStepComponent.RoutePathHint')}
+
+ }
+ >
+
+
+ {values.menuUrl || '—'}
+
+ {values.menuUrl && (
+
}
+ title={translate('::App.Platform.OpenUrl')}
+ onClick={() => window.open(values.menuUrl, '_blank')}
+ />
+ )}
+
+
+
+
+ {selectableComponents.length === 0 && (
+
+ {translate('::App.WizardStepComponent.NoComponents')}
+
+ )}
+
+ {/* ── Önizleme ───────────────────────────────────────────────── */}
+ {values.customComponentName ? (
+
+ {selectedComponent.description.trim()}
+
+ ) : null
+ }
+ >
+
+ {compiling ? (
+
+ {translate('::App.Platform.LoadingWithThreeDot')}
+
+ ) : compileError ? (
+
+ {compileError}
+
+ ) : (
+ renderComponent(values.customComponentName)
+ )}
+
+
+ ) : (
+ selectableComponents.length > 0 && (
+
+
+
+ {translate('::App.WizardStepComponent.SelectComponent')}
+
+
+ )
+ )}
+
+ {/* ─── Fixed Footer ─────────────────────────────── */}
+
+
+
} onClick={onBack}>
+ {translate('::App.Platform.Back')}
+
+
+ {!canGo && (
+
+ ⚠ {translate('::App.ListFormWizard.Required')}: {stepMissing.join(', ')}
+
+ )}
+ }
+ disabled={!canGo}
+ onClick={onNext}
+ >
+ {translate('::App.Platform.Next')}
+
+
+
+
+
+ )
+}
+
+export default WizardStepCustomComponent
diff --git a/ui/src/views/shared/MenuAddDialog.tsx b/ui/src/views/shared/MenuAddDialog.tsx
index 1e1a5a2c..8ac0efcd 100644
--- a/ui/src/views/shared/MenuAddDialog.tsx
+++ b/ui/src/views/shared/MenuAddDialog.tsx
@@ -4,7 +4,7 @@ import { MenuItem } from '@/proxy/menus/menu'
import { MenuService } from '@/services/menu.service'
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
import { useEffect, useMemo, useRef, useState } from 'react'
-import { FaChevronDown, FaPlus } from 'react-icons/fa'
+import { FaChevronDown, FaEdit, FaPlus } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useConfig } from '@/components/ui/ConfigProvider'
import { useForm } from '@/components/ui/Form/context'
@@ -83,9 +83,7 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
{value}
) : (
-
- {translate('::App.WizardStep1.SelectIcon')}
-
+ {translate('::App.WizardStep1.SelectIcon')}
)}
void | Promise
}
+/** Düzenleme modunda menü kendi alt ağacına taşınamaz; aksi halde döngü oluşur. */
+function collectDescendantCodes(
+ items: (MenuItem & { id?: string })[],
+ rootCode: string,
+): Set {
+ const result = new Set([rootCode])
+ let grew = true
+ while (grew) {
+ grew = false
+ for (const item of items) {
+ if (!item.code || result.has(item.code)) continue
+ if (item.parentCode && result.has(item.parentCode)) {
+ result.add(item.code)
+ grew = true
+ }
+ }
+ }
+ return result
+}
+
+const emptyForm = (parentCode: string, order: number) => ({
+ name: '',
+ code: '',
+ menuTextEn: '',
+ menuTextTr: '',
+ parentCode,
+ icon: '',
+ shortName: '',
+ order,
+ url: '',
+ isDisabled: false,
+})
+
export function MenuAddDialog({
isOpen,
onClose,
initialParentCode,
initialOrder,
rawItems,
+ editMenu,
onSaved,
}: MenuAddDialogProps) {
- const [form, setForm] = useState({
- name: '',
- code: '',
- menuTextEn: '',
- menuTextTr: '',
- parentCode: initialParentCode,
- icon: '',
- shortName: '',
- order: initialOrder,
- })
+ const isEditMode = !!editMenu?.id
+ const [form, setForm] = useState(() => emptyForm(initialParentCode, initialOrder))
const [saving, setSaving] = useState(false)
const { translate } = useLocalization()
useEffect(() => {
- if (isOpen)
+ if (!isOpen) return
+ if (editMenu?.id) {
setForm({
- name: '',
- code: '',
- menuTextEn: '',
- menuTextTr: '',
- parentCode: initialParentCode,
- icon: '',
- shortName: '',
- order: initialOrder,
+ name: editMenu.code ?? '',
+ code: editMenu.code ?? '',
+ menuTextEn: editMenu.menuTextEn ?? '',
+ menuTextTr: editMenu.menuTextTr ?? '',
+ parentCode: editMenu.parentCode ?? '',
+ icon: editMenu.icon ?? '',
+ shortName: editMenu.shortName ?? '',
+ order: editMenu.order ?? 0,
+ url: editMenu.url ?? '',
+ isDisabled: editMenu.isDisabled ?? false,
})
- }, [isOpen, initialParentCode, initialOrder])
+ return
+ }
+ setForm(emptyForm(initialParentCode, initialOrder))
+ }, [isOpen, initialParentCode, initialOrder, editMenu])
+ // Kök menülerde kısa ad zorunlu; alt menülerde isteğe bağlıdır.
const shortNameRequired = !form.parentCode.trim()
+
+ // Düzenlenen menü, kendisi veya alt ağacındaki bir menünün altına taşınamaz.
+ const parentOptions = useMemo(() => {
+ if (!isEditMode) return []
+ const blocked = collectDescendantCodes(rawItems, editMenu?.code ?? '')
+ return rawItems
+ .filter((item) => item.code && !blocked.has(item.code))
+ .map((item) => ({ code: item.code!, label: item.code! }))
+ .sort((a, b) => a.label.localeCompare(b.label))
+ }, [isEditMode, rawItems, editMenu])
+
const handleSave = async () => {
if (!form.code.trim() || !form.menuTextEn.trim()) return
if (shortNameRequired && !form.shortName.trim()) return
@@ -206,17 +253,27 @@ export function MenuAddDialog({
try {
const savedMenu = {
code: form.code.trim(),
+ // Görünen ad dil anahtarıdır; menü kodu ile aynı kalır.
displayName: form.code.trim(),
parentCode: form.parentCode.trim() || undefined,
icon: form.icon || undefined,
shortName: form.shortName.trim() || undefined,
order: form.order,
- isDisabled: false,
+ isDisabled: form.isDisabled,
+ url: form.url.trim() || undefined,
menuTextTr: form.menuTextTr.trim(),
menuTextEn: form.menuTextEn.trim(),
} as MenuDto
- await menuService.createWithLanguageKeyText(savedMenu)
+ if (isEditMode) {
+ // Kod ve görünen ad dışındaki tüm alanlar güncellenir; sunucu kodu korur.
+ await menuService.updateWithLanguageKeyText(editMenu!.id!, {
+ ...(editMenu as MenuDto),
+ ...savedMenu,
+ })
+ } else {
+ await menuService.createWithLanguageKeyText(savedMenu)
+ }
await onSaved({
code: savedMenu.code!,
@@ -236,9 +293,6 @@ export function MenuAddDialog({
}
}
- // suppress unused warning — rawItems kept for future use
- void rawItems
-
const fieldCls =
'h-9 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 outline-none focus:border-indigo-400 w-full'
const disabledCls =
@@ -257,9 +311,15 @@ export function MenuAddDialog({
{/* Header */}
-
+ {isEditMode ? (
+
+ ) : (
+
+ )}
- {translate('::App.Platform.AddNewMenu')}
+ {isEditMode
+ ? translate('::App.Platform.EditMenu')
+ : translate('::App.Platform.AddNewMenu')}
@@ -270,8 +330,10 @@ export function MenuAddDialog({
{translate('::App.Listform.ListformField.Name')}{' '}
*
+ {/* Ad, kodu ve dil anahtarını türetir; düzenlemede kod sabit olduğu için kilitlidir. */}
{
const codeToken = toCodeToken(e.target.value)
@@ -287,7 +349,7 @@ export function MenuAddDialog({
}))
}}
placeholder="MyMenu"
- className={fieldCls}
+ className={isEditMode ? disabledCls : fieldCls}
/>
@@ -346,7 +408,22 @@ export function MenuAddDialog({
-
+ {isEditMode ? (
+
+ ) : (
+
+ )}
@@ -378,12 +455,43 @@ export function MenuAddDialog({
className={`${fieldCls}`}
/>
{shortNameRequired && (
-
- {translate('::App.Platform.MenuCodeHint')}
-
+
{translate('::App.Platform.MenuCodeHint')}
)}
+ {/* Row 6 — Url | Durum (yalnızca düzenlemede) */}
+ {isEditMode && (
+
+ )}
+
{/* Footer */}