From aac3f4aa80c51158f8f3bb03625761768c8116e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:43:03 +0300 Subject: [PATCH] =?UTF-8?q?Wizard=20Step1=20g=C3=BCncellemesi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ListForms/Wizard/WizardCreateInputDto.cs | 1 + .../Seeds/LanguagesData.json | 12 + ui/src/proxy/admin/list-form/models.ts | 1 + ui/src/views/admin/listForm/Wizard.tsx | 361 ++++------ ui/src/views/admin/listForm/WizardStep1.tsx | 662 ++++++++++++++++++ 5 files changed, 818 insertions(+), 219 deletions(-) create mode 100644 ui/src/views/admin/listForm/WizardStep1.tsx diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardCreateInputDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardCreateInputDto.cs index 3a02502..755a749 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardCreateInputDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardCreateInputDto.cs @@ -6,6 +6,7 @@ namespace Sozsoft.Platform.ListForms; public class WizardCreateInputDto { public string ListFormCode { get; set; } + public string MenuCode { get; set; } public string LanguageTextMenuEn { get; set; } public string LanguageTextMenuTr { get; set; } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index 5fcfbea..89e0153 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -1404,6 +1404,18 @@ "en": "Wizard", "tr": "Sihirbaz" }, + { + "resourceName": "Platform", + "key": "ListForms.Wizard.MenuInfo", + "en": "Menu Information", + "tr": "Menü Bilgileri" + }, + { + "resourceName": "Platform", + "key": "ListForms.Wizard.ListFormSettings", + "en": "List Form Settings", + "tr": "Liste Formu Ayarları" + }, { "resourceName": "Platform", "key": "App.Listforms.DataSource", diff --git a/ui/src/proxy/admin/list-form/models.ts b/ui/src/proxy/admin/list-form/models.ts index 91b145a..b1b109f 100644 --- a/ui/src/proxy/admin/list-form/models.ts +++ b/ui/src/proxy/admin/list-form/models.ts @@ -17,6 +17,7 @@ import { export interface ListFormWizardDto { listFormCode: string + menuCode: string languageTextMenuEn: string languageTextMenuTr: string languageTextTitleEn: string diff --git a/ui/src/views/admin/listForm/Wizard.tsx b/ui/src/views/admin/listForm/Wizard.tsx index ef7f2e1..6589679 100644 --- a/ui/src/views/admin/listForm/Wizard.tsx +++ b/ui/src/views/admin/listForm/Wizard.tsx @@ -1,4 +1,4 @@ -import Container from '@/components/shared/Container' +import Container from '@/components/shared/Container' import { Button, FormContainer, @@ -26,40 +26,49 @@ import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models' import { postListFormWizard } from '@/services/admin/list-form.service' import { getDataSources } from '@/services/data-source.service' import { APP_NAME } from '@/constants/app.constant' +import { MenuItem } from '@/proxy/menus/menu' +import WizardStep1, { + MenuTreeNode, + buildMenuTree, + filterNonLinkNodes, + findRootCode, +} from './WizardStep1' + + +// ─── Formik initial values & validation ────────────────────────────────────── const initialValues: ListFormWizardDto = { listFormCode: '', + menuCode: '', languageTextMenuEn: '', languageTextMenuTr: '', languageTextTitleEn: '', languageTextTitleTr: '', languageTextDescEn: '', languageTextDescTr: '', - languageTextMenuParentEn: '', // Cascade: menuParentCode - languageTextMenuParentTr: '', // Cascade: menuParentCode - permissionGroupName: '', // Creatable - menuParentCode: '', // Creatable - menuIcon: '', // Select - dataSourceCode: '', // Select - dataSourceConnectionString: '', // Cascade: dataSourceCode - selectCommandType: SelectCommandTypeEnum.Table, // Select: Enum SelectCommandTypeEnum + languageTextMenuParentEn: '', + languageTextMenuParentTr: '', + permissionGroupName: '', + menuParentCode: '', + menuIcon: '', + dataSourceCode: '', + dataSourceConnectionString: '', + selectCommandType: SelectCommandTypeEnum.Table, selectCommand: '', keyFieldName: '', - keyFieldDbSourceType: DbTypeEnum.Int32, // Select: Enum dbSourceTypeOptions + keyFieldDbSourceType: DbTypeEnum.Int32, } const step1ValidationSchema = Yup.object().shape({ - listFormCode: Yup.string().required(), + menuCode: Yup.string().required(), permissionGroupName: Yup.string().required(), menuParentCode: Yup.string().required(), - menuIcon: Yup.string(), - languageTextMenuEn: Yup.string(), - languageTextMenuTr: Yup.string(), - languageTextMenuParentEn: Yup.string(), - languageTextMenuParentTr: Yup.string(), + languageTextMenuEn: Yup.string().required(), + languageTextMenuTr: Yup.string().required(), }) const step2ValidationSchema = Yup.object().shape({ + listFormCode: Yup.string().required(), languageTextTitleEn: Yup.string(), languageTextTitleTr: Yup.string(), languageTextDescEn: Yup.string(), @@ -74,10 +83,13 @@ const step2ValidationSchema = Yup.object().shape({ const listFormValidationSchema = step1ValidationSchema.concat(step2ValidationSchema) -const Wizard = () => { - const { translate } = useLocalization() - const [currentStep, setCurrentStep] = useState(0) +// ─── Wizard ─────────────────────────────────────────────────────────────────── +const Wizard = () => { + const [currentStep, setCurrentStep] = useState(0) + const [wizardName, setWizardName] = useState('') + + // ── Data Source ── const [isLoadingDataSource, setIsLoadingDataSource] = useState(false) const [dataSourceList, setDataSourceList] = useState([]) const [isDataSourceNew, setIsDataSourceNew] = useState(false) @@ -86,32 +98,30 @@ const Wizard = () => { const response = await getDataSources() if (response.data?.items) { setDataSourceList( - response.data.items.map((item: any) => ({ - value: item.code, - label: item.code, - })), + response.data.items.map((item: any) => ({ value: item.code, label: item.code })), ) } setIsLoadingDataSource(false) } + // ── Menu ── const [isLoadingMenu, setIsLoadingMenu] = useState(false) - const [menuList, setMenuList] = useState([]) - const [isMenuNew, setIsMenuNew] = useState(false) + const [menuTree, setMenuTree] = useState([]) + const [rawMenuItems, setRawMenuItems] = useState<(MenuItem & { id?: string })[]>([]) + const { translate } = useLocalization() const getMenuList = async () => { setIsLoadingMenu(true) const response = await getMenus() if (response.data?.items) { - setMenuList( - response.data.items.map((item: any) => ({ - value: item.code, - label: item.displayName, - })), - ) + const items = response.data.items as (MenuItem & { id?: string })[] + setRawMenuItems(items) + const fullTree = buildMenuTree(items) + setMenuTree(filterNonLinkNodes(fullTree)) } setIsLoadingMenu(false) } + // ── Permission Groups ── const [isLoadingPermissionGroup, setIsLoadingPermissionGroup] = useState(false) const [permissionGroupList, setPermissionGroupList] = useState([]) const getPermissionGroupList = async () => { @@ -119,10 +129,7 @@ const Wizard = () => { const response = await getPermissions('R', '') if (response.data?.groups) { setPermissionGroupList( - response.data.groups.map((item: any) => ({ - value: item.name, - label: item.displayName, - })), + response.data.groups.map((item: any) => ({ value: item.name, label: item.displayName })), ) } setIsLoadingPermissionGroup(false) @@ -137,25 +144,73 @@ const Wizard = () => { const navigate = useNavigate() const formikRef = useRef>(null) + // Auto-derive listFormCode from wizardName + const deriveListFormCode = (name: string) => { + const sanitized = name.replace(/[^a-zA-Z0-9]/g, '') + return sanitized ? `App.${sanitized}` : '' + } + + const handleWizardNameChange = (name: string) => { + setWizardName(name) + const derived = deriveListFormCode(name) + formikRef.current?.setFieldValue('listFormCode', derived) + formikRef.current?.setFieldValue('menuCode', derived) + } + + const handleMenuParentChange = (code: string) => { + formikRef.current?.setFieldValue('menuParentCode', code) + if (!code) return + const rootCode = findRootCode(rawMenuItems, code) + const rootItem = rawMenuItems.find((i) => i.code === rootCode) + + // 1. Use group field if set + if (rootItem?.group) { + formikRef.current?.setFieldValue('permissionGroupName', rootItem.group) + return + } + + // 2. Match root code exactly against permission group names (e.g. "App.Saas" -> "App.Saas") + const exactMatch = permissionGroupList.find((g) => g.value === rootCode) + if (exactMatch) { + formikRef.current?.setFieldValue('permissionGroupName', exactMatch.value) + return + } + + // 3. Partial: find a group whose name contains the last segment of the root code + const lastSegment = rootCode.split('.').pop()?.toLowerCase() ?? '' + if (lastSegment) { + const partialMatch = permissionGroupList.find((g) => + g.value?.toLowerCase().includes(lastSegment), + ) + if (partialMatch) { + formikRef.current?.setFieldValue('permissionGroupName', partialMatch.value) + return + } + } + + // 4. Fallback: set rootCode as a new creatable value (SelectBox supports free input) + formikRef.current?.setFieldValue('permissionGroupName', rootCode) + } + const handleNext = async () => { if (!formikRef.current) return const errors = await formikRef.current.validateForm() const step1Fields = Object.keys(step1ValidationSchema.fields) const hasStep1Errors = step1Fields.some((f) => errors[f as keyof ListFormWizardDto]) - // Touch all step 1 fields so errors appear + + // Always mark step1 fields touched so validation errors become visible const touchedStep1 = step1Fields.reduce( (acc, key) => ({ ...acc, [key]: true }), {} as Record, ) - formikRef.current.setTouched({ ...formikRef.current.touched, ...touchedStep1 }) - if (!hasStep1Errors) { - setCurrentStep(1) - } + await formikRef.current.setTouched({ ...formikRef.current.touched, ...touchedStep1 }) + + // Also require wizardName + if (!wizardName.trim() || hasStep1Errors) return + setCurrentStep(1) } - const handleBack = () => { - setCurrentStep(0) - } + const handleBack = () => setCurrentStep(0) return ( @@ -163,12 +218,14 @@ const Wizard = () => { titleTemplate={`%s | ${APP_NAME}`} title={translate('::' + 'App.Listforms.Wizard')} defaultTitle={APP_NAME} - > + />
- - + +
@@ -185,9 +242,7 @@ const Wizard = () => { {translate('::ListForms.FormBilgileriKaydedildi')} , - { - placement: 'top-end', - }, + { placement: 'top-end' }, ) setSubmitting(false) setTimeout(() => { @@ -210,181 +265,49 @@ const Wizard = () => { {/* ─── Step 1: Basic Info ─────────────────────────────── */} {currentStep === 0 && ( - <> - - - - - - - {({ field, form }: FieldProps) => ( - o.value === values.menuParentCode) ?? { - label: values.menuParentCode, - value: values.menuParentCode, - }) - : null - } - onChange={(option) => { - form.setFieldValue(field.name, option?.value) - setIsMenuNew( - !!option?.value && - !menuList.some((a) => a.value === option?.value), - ) - }} - /> - )} - - - - - - - - - {isMenuNew && ( -
- - - - - - -
- )} - -
- - - - - - -
- - - + formikRef.current?.setFieldValue('menuParentCode', '')} + onReloadMenu={getMenuList} + permissionGroupList={permissionGroupList} + isLoadingPermissionGroup={isLoadingPermissionGroup} + onNext={handleNext} + translate={translate} + /> )} {/* ─── Step 2: Data Settings ───────────────────────────── */} {currentStep === 1 && ( <> + {/* ListForm Code */} + + Auto-derived from Wizard Name, editable + + } + > + + +
() + const roots: MenuTreeNode[] = [] + items.forEach((item) => { + map.set(item.code!, { + code: item.code!, + displayName: item.displayName ?? item.code!, + icon: item.icon ?? undefined, + url: item.url ?? undefined, + children: [], + }) + }) + items.forEach((item) => { + const node = map.get(item.code!)! + if (item.parentCode && map.has(item.parentCode)) { + map.get(item.parentCode)!.children.push(node) + } else { + roots.push(node) + } + }) + return roots +} + +export function filterNonLinkNodes(nodes: MenuTreeNode[]): MenuTreeNode[] { + return nodes + .filter((n) => !n.url) + .map((n) => ({ ...n, children: filterNonLinkNodes(n.children) })) +} + +export function findRootCode(rawItems: MenuItem[], code: string): string { + let current = code + for (let i = 0; i < 50; i++) { + const item = rawItems.find((r) => r.code === current) + if (!item?.parentCode) return current + current = item.parentCode + } + return current +} + +// ─── MenuService singleton ──────────────────────────────────────────────────── + +const menuService = new MenuService() + +// ─── TreeNode ───────────────────────────────────────────────────────────────── + +interface TreeNodeProps { + node: MenuTreeNode & { id?: string } + depth: number + selectedCode: string + onSelect: (code: string) => void + expanded: Set + onToggle: (code: string) => void + editingCode: string | null + editingValue: string + saving: boolean + onStartEdit: (code: string, currentName: string) => void + onEditChange: (v: string) => void + onSaveEdit: (node: MenuTreeNode & { id?: string }) => void + onCancelEdit: () => void + onDelete: (node: MenuTreeNode & { id?: string }) => void +} + +function TreeNode({ + node, depth, selectedCode, onSelect, expanded, onToggle, + editingCode, editingValue, saving, + onStartEdit, onEditChange, onSaveEdit, onCancelEdit, onDelete, +}: TreeNodeProps) { + const hasChildren = node.children.length > 0 + const isExpanded = expanded.has(node.code) + const isSelected = node.code === selectedCode + const isEditing = editingCode === node.code + const NodeIcon = node.icon ? navigationIcon[node.icon] : null + const { translate } = useLocalization() + + return ( +
+
+ { e.stopPropagation(); if (hasChildren) onToggle(node.code) }} + > + {hasChildren ? ( + isExpanded + ? + : + ) : null} + + + {NodeIcon && ( + + )} + + {isEditing ? ( + onEditChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') onSaveEdit(node) + if (e.key === 'Escape') onCancelEdit() + }} + onClick={(e) => e.stopPropagation()} + className="flex-1 px-2 py-0.5 text-sm rounded border border-indigo-400 outline-none bg-white text-gray-800 dark:bg-gray-700 dark:text-gray-100" + /> + ) : ( + onSelect(node.code)}> + {node.displayName} + + )} + + !isEditing && onSelect(node.code)} + > + {translate('::' + node.code)} + + + {isEditing ? ( + <> + + + + ) : ( + + + + + )} +
+ + {isExpanded && node.children.map((child) => ( + + ))} +
+ ) +} + +// ─── MenuTreeInline ─────────────────────────────────────────────────────────── + +interface MenuTreeInlineProps { + value: string + onChange: (code: string) => void + nodes: MenuTreeNode[] + rawItems: (MenuItem & { id?: string })[] + isLoading: boolean + invalid?: boolean + onReload: () => void +} + +function MenuTreeInline({ value, onChange, nodes, rawItems, isLoading, invalid, onReload }: MenuTreeInlineProps) { + const [expanded, setExpanded] = useState>(new Set()) + const [editingCode, setEditingCode] = useState(null) + const [editingValue, setEditingValue] = useState('') + const [saving, setSaving] = useState(false) + + const toggle = (code: string) => + setExpanded((prev) => { const n = new Set(prev); n.has(code) ? n.delete(code) : n.add(code); return n }) + + const handleStartEdit = (code: string, currentName: string) => { + setEditingCode(code) + setEditingValue(currentName) + } + + const handleSaveEdit = async (node: MenuTreeNode & { id?: string }) => { + if (!editingValue.trim() || !node.id) return + setSaving(true) + try { + const original = rawItems.find((i) => i.code === node.code) + if (!original) return + await menuService.update(node.id, { ...original, displayName: editingValue.trim() } as MenuDto) + onReload() + setEditingCode(null) + } catch (e: any) { + toast.push(, { placement: 'top-end' }) + } finally { + setSaving(false) + } + } + + const handleDelete = async (node: MenuTreeNode & { id?: string }) => { + if (!node.id) return + if (!window.confirm(`"${node.displayName}" menüsünü silmek istediğinize emin misiniz?`)) return + setSaving(true) + try { + await menuService.delete(node.id) + onReload() + } catch (e: any) { + toast.push(, { placement: 'top-end' }) + } finally { + setSaving(false) + } + } + + function enrichNode(node: MenuTreeNode): MenuTreeNode & { id?: string } { + const raw = rawItems.find((i) => i.code === node.code) + return { ...node, id: raw?.id, children: node.children.map(enrichNode) } + } + + const enrichedNodes = nodes.map(enrichNode) + + const sharedNodeProps = { + expanded, onToggle: toggle, + editingCode, editingValue, saving, + onStartEdit: handleStartEdit, onEditChange: setEditingValue, + onSaveEdit: handleSaveEdit, onCancelEdit: () => setEditingCode(null), + onDelete: handleDelete, + } + + return ( +
+
+ {isLoading ? ( +
Loading…
+ ) : enrichedNodes.length === 0 ? ( +
No menus available
+ ) : ( + enrichedNodes.map((node) => ( + + )) + )} +
+
+ ) +} + +// ─── 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(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 + + 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 ( +
+ + + {open && ( +
+
+ setSearch(e.target.value)} + placeholder="Search icons… (e.g. 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" + /> + {filtered.length} icons +
+
+ {displayed.map(([key, Icon]) => ( + + ))} +
+ {hasMore && ( +
+ {displayed.length} / {filtered.length} + +
+ )} +
+ )} +
+ ) +} + +// ─── 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({ code: '', displayName: '', parentCode: initialParentCode, icon: '', shortName: '', order: initialOrder }) + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (isOpen) setForm({ code: '', displayName: '', parentCode: initialParentCode, icon: '', shortName: '', order: initialOrder }) + }, [isOpen, initialParentCode, initialOrder]) + + const handleSave = async () => { + if (!form.code.trim() || !form.displayName.trim()) return + setSaving(true) + try { + await menuService.create({ + code: form.code.trim(), + displayName: form.displayName.trim(), + parentCode: form.parentCode.trim() || undefined, + icon: form.icon || undefined, + shortName: form.shortName.trim() || undefined, + order: form.order, + isDisabled: false, + } as MenuDto) + onSaved() + onClose() + } catch (e: any) { + toast.push(, { placement: 'top-end' }) + } finally { + setSaving(false) + } + } + + // suppress unused warning — rawItems kept for future use + void rawItems + + return ( + +
+
Yeni Menü Ekle
+
+
+ + setForm((p) => ({ ...p, code: e.target.value }))} placeholder="App.MyMenu" + className="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" /> +
+
+ + setForm((p) => ({ ...p, displayName: e.target.value }))} placeholder="My Menu" + className="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" /> +
+
+ + setForm((p) => ({ ...p, icon: key }))} /> +
+
+ + +
+
+ + setForm((p) => ({ ...p, order: Number(e.target.value) }))} + className="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" /> +
+
+ + setForm((p) => ({ ...p, shortName: e.target.value }))} placeholder="My Menu (short)" + className="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" /> +
+
+
+ + +
+
+
+ ) +} + +// ─── WizardStep1 ────────────────────────────────────────────────────────────── + +export interface WizardStep1Props { + values: ListFormWizardDto + errors: FormikErrors + touched: FormikTouched + wizardName: string + onWizardNameChange: (name: string) => void + rawMenuItems: (MenuItem & { id?: string })[] + menuTree: MenuTreeNode[] + isLoadingMenu: boolean + onMenuParentChange: (code: string) => void + onClearMenuParent: () => void + onReloadMenu: () => void + permissionGroupList: SelectBoxOption[] + isLoadingPermissionGroup: boolean + onNext: () => void + translate: (key: string) => string +} + +const WizardStep1 = ({ + values, errors, touched, + wizardName, onWizardNameChange, + rawMenuItems, menuTree, isLoadingMenu, + onMenuParentChange, onClearMenuParent, onReloadMenu, + permissionGroupList, isLoadingPermissionGroup, + onNext, translate, +}: WizardStep1Props) => { + const [menuDialogOpen, setMenuDialogOpen] = useState(false) + const [menuDialogParentCode, setMenuDialogParentCode] = useState('') + const [menuDialogInitialOrder, setMenuDialogInitialOrder] = useState(999) + + return ( + <> + {/* Wizard Name */} + Used to generate ListForm Code and Menu Code} + > + onWizardNameChange(e.target.value)} + /> + + + {/* Menu Parent */} + + + {values.menuParentCode && ( + + )} +
+ } + > + + {() => ( + + )} + + + + setMenuDialogOpen(false)} + initialParentCode={menuDialogParentCode} + initialOrder={menuDialogInitialOrder} + rawItems={rawMenuItems} + onSaved={onReloadMenu} + /> + + {/* Menu Code */} + Auto-derived, editable} + > + + + + + + + + + + + + {/* Permission Group Name */} + + + {({ field, form }: FieldProps) => ( +