Wizard Step1 güncellemesi
This commit is contained in:
parent
877d0e7397
commit
aac3f4aa80
5 changed files with 818 additions and 219 deletions
|
|
@ -6,6 +6,7 @@ namespace Sozsoft.Platform.ListForms;
|
||||||
public class WizardCreateInputDto
|
public class WizardCreateInputDto
|
||||||
{
|
{
|
||||||
public string ListFormCode { get; set; }
|
public string ListFormCode { get; set; }
|
||||||
|
public string MenuCode { get; set; }
|
||||||
|
|
||||||
public string LanguageTextMenuEn { get; set; }
|
public string LanguageTextMenuEn { get; set; }
|
||||||
public string LanguageTextMenuTr { get; set; }
|
public string LanguageTextMenuTr { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1404,6 +1404,18 @@
|
||||||
"en": "Wizard",
|
"en": "Wizard",
|
||||||
"tr": "Sihirbaz"
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.Listforms.DataSource",
|
"key": "App.Listforms.DataSource",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
|
|
||||||
export interface ListFormWizardDto {
|
export interface ListFormWizardDto {
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
|
menuCode: string
|
||||||
languageTextMenuEn: string
|
languageTextMenuEn: string
|
||||||
languageTextMenuTr: string
|
languageTextMenuTr: string
|
||||||
languageTextTitleEn: string
|
languageTextTitleEn: string
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Container from '@/components/shared/Container'
|
import Container from '@/components/shared/Container'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormContainer,
|
FormContainer,
|
||||||
|
|
@ -26,40 +26,49 @@ import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
|
||||||
import { postListFormWizard } from '@/services/admin/list-form.service'
|
import { postListFormWizard } from '@/services/admin/list-form.service'
|
||||||
import { getDataSources } from '@/services/data-source.service'
|
import { getDataSources } from '@/services/data-source.service'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
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 = {
|
const initialValues: ListFormWizardDto = {
|
||||||
listFormCode: '',
|
listFormCode: '',
|
||||||
|
menuCode: '',
|
||||||
languageTextMenuEn: '',
|
languageTextMenuEn: '',
|
||||||
languageTextMenuTr: '',
|
languageTextMenuTr: '',
|
||||||
languageTextTitleEn: '',
|
languageTextTitleEn: '',
|
||||||
languageTextTitleTr: '',
|
languageTextTitleTr: '',
|
||||||
languageTextDescEn: '',
|
languageTextDescEn: '',
|
||||||
languageTextDescTr: '',
|
languageTextDescTr: '',
|
||||||
languageTextMenuParentEn: '', // Cascade: menuParentCode
|
languageTextMenuParentEn: '',
|
||||||
languageTextMenuParentTr: '', // Cascade: menuParentCode
|
languageTextMenuParentTr: '',
|
||||||
permissionGroupName: '', // Creatable
|
permissionGroupName: '',
|
||||||
menuParentCode: '', // Creatable
|
menuParentCode: '',
|
||||||
menuIcon: '', // Select
|
menuIcon: '',
|
||||||
dataSourceCode: '', // Select
|
dataSourceCode: '',
|
||||||
dataSourceConnectionString: '', // Cascade: dataSourceCode
|
dataSourceConnectionString: '',
|
||||||
selectCommandType: SelectCommandTypeEnum.Table, // Select: Enum SelectCommandTypeEnum
|
selectCommandType: SelectCommandTypeEnum.Table,
|
||||||
selectCommand: '',
|
selectCommand: '',
|
||||||
keyFieldName: '',
|
keyFieldName: '',
|
||||||
keyFieldDbSourceType: DbTypeEnum.Int32, // Select: Enum dbSourceTypeOptions
|
keyFieldDbSourceType: DbTypeEnum.Int32,
|
||||||
}
|
}
|
||||||
|
|
||||||
const step1ValidationSchema = Yup.object().shape({
|
const step1ValidationSchema = Yup.object().shape({
|
||||||
listFormCode: Yup.string().required(),
|
menuCode: Yup.string().required(),
|
||||||
permissionGroupName: Yup.string().required(),
|
permissionGroupName: Yup.string().required(),
|
||||||
menuParentCode: Yup.string().required(),
|
menuParentCode: Yup.string().required(),
|
||||||
menuIcon: Yup.string(),
|
languageTextMenuEn: Yup.string().required(),
|
||||||
languageTextMenuEn: Yup.string(),
|
languageTextMenuTr: Yup.string().required(),
|
||||||
languageTextMenuTr: Yup.string(),
|
|
||||||
languageTextMenuParentEn: Yup.string(),
|
|
||||||
languageTextMenuParentTr: Yup.string(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const step2ValidationSchema = Yup.object().shape({
|
const step2ValidationSchema = Yup.object().shape({
|
||||||
|
listFormCode: Yup.string().required(),
|
||||||
languageTextTitleEn: Yup.string(),
|
languageTextTitleEn: Yup.string(),
|
||||||
languageTextTitleTr: Yup.string(),
|
languageTextTitleTr: Yup.string(),
|
||||||
languageTextDescEn: Yup.string(),
|
languageTextDescEn: Yup.string(),
|
||||||
|
|
@ -74,10 +83,13 @@ const step2ValidationSchema = Yup.object().shape({
|
||||||
|
|
||||||
const listFormValidationSchema = step1ValidationSchema.concat(step2ValidationSchema)
|
const listFormValidationSchema = step1ValidationSchema.concat(step2ValidationSchema)
|
||||||
|
|
||||||
const Wizard = () => {
|
// ─── Wizard ───────────────────────────────────────────────────────────────────
|
||||||
const { translate } = useLocalization()
|
|
||||||
const [currentStep, setCurrentStep] = useState(0)
|
|
||||||
|
|
||||||
|
const Wizard = () => {
|
||||||
|
const [currentStep, setCurrentStep] = useState(0)
|
||||||
|
const [wizardName, setWizardName] = useState('')
|
||||||
|
|
||||||
|
// ── Data Source ──
|
||||||
const [isLoadingDataSource, setIsLoadingDataSource] = useState(false)
|
const [isLoadingDataSource, setIsLoadingDataSource] = useState(false)
|
||||||
const [dataSourceList, setDataSourceList] = useState<SelectBoxOption[]>([])
|
const [dataSourceList, setDataSourceList] = useState<SelectBoxOption[]>([])
|
||||||
const [isDataSourceNew, setIsDataSourceNew] = useState(false)
|
const [isDataSourceNew, setIsDataSourceNew] = useState(false)
|
||||||
|
|
@ -86,32 +98,30 @@ const Wizard = () => {
|
||||||
const response = await getDataSources()
|
const response = await getDataSources()
|
||||||
if (response.data?.items) {
|
if (response.data?.items) {
|
||||||
setDataSourceList(
|
setDataSourceList(
|
||||||
response.data.items.map((item: any) => ({
|
response.data.items.map((item: any) => ({ value: item.code, label: item.code })),
|
||||||
value: item.code,
|
|
||||||
label: item.code,
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setIsLoadingDataSource(false)
|
setIsLoadingDataSource(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Menu ──
|
||||||
const [isLoadingMenu, setIsLoadingMenu] = useState(false)
|
const [isLoadingMenu, setIsLoadingMenu] = useState(false)
|
||||||
const [menuList, setMenuList] = useState<SelectBoxOption[]>([])
|
const [menuTree, setMenuTree] = useState<MenuTreeNode[]>([])
|
||||||
const [isMenuNew, setIsMenuNew] = useState(false)
|
const [rawMenuItems, setRawMenuItems] = useState<(MenuItem & { id?: string })[]>([])
|
||||||
|
const { translate } = useLocalization()
|
||||||
const getMenuList = async () => {
|
const getMenuList = async () => {
|
||||||
setIsLoadingMenu(true)
|
setIsLoadingMenu(true)
|
||||||
const response = await getMenus()
|
const response = await getMenus()
|
||||||
if (response.data?.items) {
|
if (response.data?.items) {
|
||||||
setMenuList(
|
const items = response.data.items as (MenuItem & { id?: string })[]
|
||||||
response.data.items.map((item: any) => ({
|
setRawMenuItems(items)
|
||||||
value: item.code,
|
const fullTree = buildMenuTree(items)
|
||||||
label: item.displayName,
|
setMenuTree(filterNonLinkNodes(fullTree))
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
setIsLoadingMenu(false)
|
setIsLoadingMenu(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Permission Groups ──
|
||||||
const [isLoadingPermissionGroup, setIsLoadingPermissionGroup] = useState(false)
|
const [isLoadingPermissionGroup, setIsLoadingPermissionGroup] = useState(false)
|
||||||
const [permissionGroupList, setPermissionGroupList] = useState<SelectBoxOption[]>([])
|
const [permissionGroupList, setPermissionGroupList] = useState<SelectBoxOption[]>([])
|
||||||
const getPermissionGroupList = async () => {
|
const getPermissionGroupList = async () => {
|
||||||
|
|
@ -119,10 +129,7 @@ const Wizard = () => {
|
||||||
const response = await getPermissions('R', '')
|
const response = await getPermissions('R', '')
|
||||||
if (response.data?.groups) {
|
if (response.data?.groups) {
|
||||||
setPermissionGroupList(
|
setPermissionGroupList(
|
||||||
response.data.groups.map((item: any) => ({
|
response.data.groups.map((item: any) => ({ value: item.name, label: item.displayName })),
|
||||||
value: item.name,
|
|
||||||
label: item.displayName,
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setIsLoadingPermissionGroup(false)
|
setIsLoadingPermissionGroup(false)
|
||||||
|
|
@ -137,25 +144,73 @@ const Wizard = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const formikRef = useRef<FormikProps<ListFormWizardDto>>(null)
|
const formikRef = useRef<FormikProps<ListFormWizardDto>>(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 () => {
|
const handleNext = async () => {
|
||||||
if (!formikRef.current) return
|
if (!formikRef.current) return
|
||||||
const errors = await formikRef.current.validateForm()
|
const errors = await formikRef.current.validateForm()
|
||||||
const step1Fields = Object.keys(step1ValidationSchema.fields)
|
const step1Fields = Object.keys(step1ValidationSchema.fields)
|
||||||
const hasStep1Errors = step1Fields.some((f) => errors[f as keyof ListFormWizardDto])
|
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(
|
const touchedStep1 = step1Fields.reduce(
|
||||||
(acc, key) => ({ ...acc, [key]: true }),
|
(acc, key) => ({ ...acc, [key]: true }),
|
||||||
{} as Record<string, boolean>,
|
{} as Record<string, boolean>,
|
||||||
)
|
)
|
||||||
formikRef.current.setTouched({ ...formikRef.current.touched, ...touchedStep1 })
|
await formikRef.current.setTouched({ ...formikRef.current.touched, ...touchedStep1 })
|
||||||
if (!hasStep1Errors) {
|
|
||||||
|
// Also require wizardName
|
||||||
|
if (!wizardName.trim() || hasStep1Errors) return
|
||||||
setCurrentStep(1)
|
setCurrentStep(1)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => setCurrentStep(0)
|
||||||
setCurrentStep(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
|
@ -163,12 +218,14 @@ const Wizard = () => {
|
||||||
titleTemplate={`%s | ${APP_NAME}`}
|
titleTemplate={`%s | ${APP_NAME}`}
|
||||||
title={translate('::' + 'App.Listforms.Wizard')}
|
title={translate('::' + 'App.Listforms.Wizard')}
|
||||||
defaultTitle={APP_NAME}
|
defaultTitle={APP_NAME}
|
||||||
></Helmet>
|
/>
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<Steps current={currentStep}>
|
<Steps current={currentStep}>
|
||||||
<Steps.Item title={translate('::ListForms.Wizard.BasicInfo') || 'Basic Info'} />
|
<Steps.Item title={translate('::ListForms.Wizard.MenuInfo') || 'Menu Info'} />
|
||||||
<Steps.Item title={translate('::ListForms.Wizard.DataSettings') || 'Data Settings'} />
|
<Steps.Item
|
||||||
|
title={translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings'}
|
||||||
|
/>
|
||||||
</Steps>
|
</Steps>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -185,9 +242,7 @@ const Wizard = () => {
|
||||||
<Notification type="success" duration={2000}>
|
<Notification type="success" duration={2000}>
|
||||||
{translate('::ListForms.FormBilgileriKaydedildi')}
|
{translate('::ListForms.FormBilgileriKaydedildi')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{ placement: 'top-end' },
|
||||||
placement: 'top-end',
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -210,181 +265,49 @@ const Wizard = () => {
|
||||||
<FormContainer size="sm">
|
<FormContainer size="sm">
|
||||||
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
||||||
{currentStep === 0 && (
|
{currentStep === 0 && (
|
||||||
<>
|
<WizardStep1
|
||||||
<FormItem
|
values={values}
|
||||||
label="ListForm Code"
|
errors={errors}
|
||||||
invalid={errors.listFormCode && touched.listFormCode}
|
touched={touched}
|
||||||
errorMessage={errors.listFormCode}
|
wizardName={wizardName}
|
||||||
asterisk={true}
|
onWizardNameChange={handleWizardNameChange}
|
||||||
>
|
rawMenuItems={rawMenuItems}
|
||||||
<Field
|
menuTree={menuTree}
|
||||||
type="text"
|
isLoadingMenu={isLoadingMenu}
|
||||||
autoComplete="off"
|
onMenuParentChange={handleMenuParentChange}
|
||||||
name="listFormCode"
|
onClearMenuParent={() => formikRef.current?.setFieldValue('menuParentCode', '')}
|
||||||
placeholder="ListForm Code"
|
onReloadMenu={getMenuList}
|
||||||
component={Input}
|
permissionGroupList={permissionGroupList}
|
||||||
|
isLoadingPermissionGroup={isLoadingPermissionGroup}
|
||||||
|
onNext={handleNext}
|
||||||
|
translate={translate}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Permission Group Name"
|
|
||||||
invalid={errors.permissionGroupName && touched.permissionGroupName}
|
|
||||||
errorMessage={errors.permissionGroupName}
|
|
||||||
asterisk={true}
|
|
||||||
>
|
|
||||||
<Field type="text" autoComplete="off" name="permissionGroupName">
|
|
||||||
{({ field, form }: FieldProps<string>) => (
|
|
||||||
<Select
|
|
||||||
componentAs={CreatableSelect}
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
placeholder="Permission Group Name"
|
|
||||||
isClearable={true}
|
|
||||||
isLoading={isLoadingPermissionGroup}
|
|
||||||
options={permissionGroupList}
|
|
||||||
value={
|
|
||||||
values.permissionGroupName
|
|
||||||
? (permissionGroupList?.find(
|
|
||||||
(o) => o.value === values.permissionGroupName,
|
|
||||||
) ?? {
|
|
||||||
label: values.permissionGroupName,
|
|
||||||
value: values.permissionGroupName,
|
|
||||||
})
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
onChange={(option) => {
|
|
||||||
form.setFieldValue(field.name, option?.value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<FormItem
|
|
||||||
label="Menu Parent Code"
|
|
||||||
invalid={errors.menuParentCode && touched.menuParentCode}
|
|
||||||
errorMessage={errors.menuParentCode}
|
|
||||||
asterisk={true}
|
|
||||||
>
|
|
||||||
<Field type="text" autoComplete="off" name="menuParentCode">
|
|
||||||
{({ field, form }: FieldProps<string>) => (
|
|
||||||
<Select
|
|
||||||
componentAs={CreatableSelect}
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
placeholder="Menu Parent Code"
|
|
||||||
isClearable={true}
|
|
||||||
isLoading={isLoadingMenu}
|
|
||||||
options={menuList}
|
|
||||||
value={
|
|
||||||
values.menuParentCode
|
|
||||||
? (menuList?.find((o) => 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),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Menu Icon"
|
|
||||||
invalid={errors.menuIcon && touched.menuIcon}
|
|
||||||
errorMessage={errors.menuIcon}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="menuIcon"
|
|
||||||
placeholder="Menu Icon"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isMenuNew && (
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<FormItem
|
|
||||||
label="Parent Menu Text (En)"
|
|
||||||
invalid={
|
|
||||||
errors.languageTextMenuParentEn && touched.languageTextMenuParentEn
|
|
||||||
}
|
|
||||||
errorMessage={errors.languageTextMenuParentEn}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="languageTextMenuParentEn"
|
|
||||||
placeholder="Parent Menu Text (En)"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label="Parent Menu Text (Tr)"
|
|
||||||
invalid={
|
|
||||||
errors.languageTextMenuParentTr && touched.languageTextMenuParentTr
|
|
||||||
}
|
|
||||||
errorMessage={errors.languageTextMenuParentTr}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="languageTextMenuParentTr"
|
|
||||||
placeholder="Parent Menu Text (Tr)"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<FormItem
|
|
||||||
label="Menu Text (En)"
|
|
||||||
invalid={errors.languageTextMenuEn && touched.languageTextMenuEn}
|
|
||||||
errorMessage={errors.languageTextMenuEn}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="languageTextMenuEn"
|
|
||||||
placeholder="Menu Text (En)"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label="Menu Text (Tr)"
|
|
||||||
invalid={errors.languageTextMenuTr && touched.languageTextMenuTr}
|
|
||||||
errorMessage={errors.languageTextMenuTr}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="languageTextMenuTr"
|
|
||||||
placeholder="Menu Text (Tr)"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button block className="mt-4" variant="solid" type="button" onClick={handleNext}>
|
|
||||||
{translate('::Next') || 'Next'}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Step 2: Data Settings ───────────────────────────── */}
|
{/* ─── Step 2: Data Settings ───────────────────────────── */}
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<>
|
<>
|
||||||
|
{/* ListForm Code */}
|
||||||
|
<FormItem
|
||||||
|
label="ListForm Code"
|
||||||
|
invalid={!!(errors.listFormCode && touched.listFormCode)}
|
||||||
|
errorMessage={errors.listFormCode}
|
||||||
|
asterisk={true}
|
||||||
|
extra={
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
Auto-derived from Wizard Name, editable
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="listFormCode"
|
||||||
|
placeholder="e.g. App.Soutes"
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<FormItem
|
<FormItem
|
||||||
label="Title (En)"
|
label="Title (En)"
|
||||||
|
|
|
||||||
662
ui/src/views/admin/listForm/WizardStep1.tsx
Normal file
662
ui/src/views/admin/listForm/WizardStep1.tsx
Normal file
|
|
@ -0,0 +1,662 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
Notification,
|
||||||
|
Select,
|
||||||
|
toast,
|
||||||
|
} from '@/components/ui'
|
||||||
|
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
|
||||||
|
import { MenuDto } from '@/proxy/menus/models'
|
||||||
|
import { SelectBoxOption } from '@/types/shared'
|
||||||
|
import navigationIcon from '@/proxy/menus/navigation-icon.config'
|
||||||
|
import { MenuItem } from '@/proxy/menus/menu'
|
||||||
|
import { MenuService } from '@/services/menu.service'
|
||||||
|
import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import CreatableSelect from 'react-select/creatable'
|
||||||
|
import {
|
||||||
|
FaChevronDown,
|
||||||
|
FaChevronRight,
|
||||||
|
FaEdit,
|
||||||
|
FaPlus,
|
||||||
|
FaTimes,
|
||||||
|
FaCheck,
|
||||||
|
FaTrash,
|
||||||
|
} from 'react-icons/fa'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
|
// ─── Types (exported for Wizard.tsx) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface MenuTreeNode {
|
||||||
|
code: string
|
||||||
|
displayName: string
|
||||||
|
icon?: string
|
||||||
|
url?: string
|
||||||
|
children: MenuTreeNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers (exported for Wizard.tsx) ───────────────────────────────────────
|
||||||
|
|
||||||
|
export function buildMenuTree(items: MenuItem[]): MenuTreeNode[] {
|
||||||
|
const map = new Map<string, MenuTreeNode>()
|
||||||
|
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<string>
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1 px-2 py-1.5 rounded mx-1 group ${
|
||||||
|
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` }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-4 h-4 flex items-center justify-center shrink-0 text-xs cursor-pointer"
|
||||||
|
onClick={(e) => { e.stopPropagation(); if (hasChildren) onToggle(node.code) }}
|
||||||
|
>
|
||||||
|
{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'}`} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={editingValue}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="flex-1 text-sm truncate cursor-pointer" onClick={() => onSelect(node.code)}>
|
||||||
|
{node.displayName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`text-xs shrink-0 ${isSelected ? 'text-indigo-200' : 'text-gray-400'}`}
|
||||||
|
onClick={() => !isEditing && onSelect(node.code)}
|
||||||
|
>
|
||||||
|
{translate('::' + node.code)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<button type="button" disabled={saving}
|
||||||
|
onClick={(e) => { e.stopPropagation(); onSaveEdit(node) }}
|
||||||
|
className="p-1 text-green-500 hover:text-green-600 disabled:opacity-40">
|
||||||
|
<FaCheck className="text-xs" />
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onCancelEdit() }}
|
||||||
|
className="p-1 text-gray-400 hover:text-gray-600">
|
||||||
|
<FaTimes className="text-xs" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="shrink-0 flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button type="button" title="Rename"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onStartEdit(node.code, node.displayName) }}
|
||||||
|
className={`p-1 ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-indigo-500'}`}>
|
||||||
|
<FaEdit className="text-xs" />
|
||||||
|
</button>
|
||||||
|
<button type="button" title="Delete"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onDelete(node) }}
|
||||||
|
className={`p-1 ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-red-500'}`}>
|
||||||
|
<FaTrash className="text-xs" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && node.children.map((child) => (
|
||||||
|
<TreeNode
|
||||||
|
key={child.code}
|
||||||
|
node={child as MenuTreeNode & { id?: string }}
|
||||||
|
depth={depth + 1}
|
||||||
|
selectedCode={selectedCode}
|
||||||
|
onSelect={onSelect}
|
||||||
|
expanded={expanded}
|
||||||
|
onToggle={onToggle}
|
||||||
|
editingCode={editingCode}
|
||||||
|
editingValue={editingValue}
|
||||||
|
saving={saving}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onEditChange={onEditChange}
|
||||||
|
onSaveEdit={onSaveEdit}
|
||||||
|
onCancelEdit={onCancelEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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<Set<string>>(new Set())
|
||||||
|
const [editingCode, setEditingCode] = useState<string | null>(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(<Notification title={e.message} type="danger" />, { 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(<Notification title={e.message} type="danger" />, { 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 (
|
||||||
|
<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-48 overflow-y-auto py-1">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="px-4 py-3 text-sm text-gray-400">Loading…</div>
|
||||||
|
) : enrichedNodes.length === 0 ? (
|
||||||
|
<div className="px-4 py-3 text-sm text-gray-400">No menus available</div>
|
||||||
|
) : (
|
||||||
|
enrichedNodes.map((node) => (
|
||||||
|
<TreeNode key={node.code} node={node} depth={0} selectedCode={value} onSelect={onChange} {...sharedNodeProps} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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
|
||||||
|
|
||||||
|
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">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… (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"
|
||||||
|
/>
|
||||||
|
<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({ 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(<Notification title={e.message} type="danger" />, { placement: 'top-end' })
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// suppress unused warning — rawItems kept for future use
|
||||||
|
void rawItems
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog isOpen={isOpen} onClose={onClose} onRequestClose={onClose} width={520}>
|
||||||
|
<div className="flex flex-col gap-4 p-4">
|
||||||
|
<h5 className="text-base font-semibold text-gray-800 dark:text-gray-100">Yeni Menü Ekle</h5>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">Code <span className="text-red-500">*</span></label>
|
||||||
|
<input value={form.code} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">Display Name <span className="text-red-500">*</span></label>
|
||||||
|
<input value={form.displayName} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">Icon</label>
|
||||||
|
<IconPickerField value={form.icon} onChange={(key) => setForm((p) => ({ ...p, icon: key }))} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">Menu Parent</label>
|
||||||
|
<input disabled value={form.parentCode || '(Ana Menü)'}
|
||||||
|
className="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" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">Sıra (Order)</label>
|
||||||
|
<input type="number" value={form.order} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">Short Name</label>
|
||||||
|
<input value={form.shortName} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<Button size="sm" variant="plain" onClick={onClose}>İptal</Button>
|
||||||
|
<Button size="sm" variant="solid" loading={saving} disabled={!form.code.trim() || !form.displayName.trim()} onClick={handleSave}>Kaydet</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── WizardStep1 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface WizardStep1Props {
|
||||||
|
values: ListFormWizardDto
|
||||||
|
errors: FormikErrors<ListFormWizardDto>
|
||||||
|
touched: FormikTouched<ListFormWizardDto>
|
||||||
|
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 */}
|
||||||
|
<FormItem
|
||||||
|
label="Wizard Name"
|
||||||
|
asterisk={true}
|
||||||
|
extra={<span className="text-xs ml-2 text-gray-400">Used to generate ListForm Code and Menu Code</span>}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="e.g. Soutes"
|
||||||
|
value={wizardName}
|
||||||
|
onChange={(e) => onWizardNameChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
{/* Menu Parent */}
|
||||||
|
<FormItem
|
||||||
|
label="Menu Parent"
|
||||||
|
invalid={errors.menuParentCode && touched.menuParentCode}
|
||||||
|
errorMessage={errors.menuParentCode}
|
||||||
|
asterisk={true}
|
||||||
|
extra={
|
||||||
|
<div className="flex items-center gap-2 ml-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setMenuDialogParentCode(values.menuParentCode ? findRootCode(rawMenuItems, values.menuParentCode) : '')
|
||||||
|
const selectedItem = rawMenuItems.find((i) => i.code === values.menuParentCode)
|
||||||
|
setMenuDialogInitialOrder(selectedItem?.order ?? 999)
|
||||||
|
setMenuDialogOpen(true)
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-green-500 text-white hover:bg-green-600"
|
||||||
|
>
|
||||||
|
<FaPlus className="text-xs" /> Ekle
|
||||||
|
</button>
|
||||||
|
{values.menuParentCode && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => { e.stopPropagation(); e.preventDefault(); onClearMenuParent() }}
|
||||||
|
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" /> Seçimi Kaldır
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Field name="menuParentCode">
|
||||||
|
{() => (
|
||||||
|
<MenuTreeInline
|
||||||
|
value={values.menuParentCode}
|
||||||
|
onChange={onMenuParentChange}
|
||||||
|
nodes={menuTree}
|
||||||
|
rawItems={rawMenuItems}
|
||||||
|
isLoading={isLoadingMenu}
|
||||||
|
invalid={!!(errors.menuParentCode && touched.menuParentCode)}
|
||||||
|
onReload={onReloadMenu}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<MenuAddDialog
|
||||||
|
isOpen={menuDialogOpen}
|
||||||
|
onClose={() => setMenuDialogOpen(false)}
|
||||||
|
initialParentCode={menuDialogParentCode}
|
||||||
|
initialOrder={menuDialogInitialOrder}
|
||||||
|
rawItems={rawMenuItems}
|
||||||
|
onSaved={onReloadMenu}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Menu Code */}
|
||||||
|
<FormItem
|
||||||
|
label="Menu Code"
|
||||||
|
invalid={!!(errors.menuCode && touched.menuCode)}
|
||||||
|
errorMessage={errors.menuCode}
|
||||||
|
asterisk={true}
|
||||||
|
extra={<span className="text-xs ml-2 text-gray-400">Auto-derived, editable</span>}
|
||||||
|
>
|
||||||
|
<Field type="text" autoComplete="off" name="menuCode" placeholder="e.g. App.Soutes" component={Input} />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Menu Text (En)"
|
||||||
|
invalid={errors.languageTextMenuEn && touched.languageTextMenuEn}
|
||||||
|
errorMessage={errors.languageTextMenuEn}
|
||||||
|
>
|
||||||
|
<Field type="text" autoComplete="off" name="languageTextMenuEn" placeholder="Menu Text (En)" component={Input} />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Menu Text (Tr)"
|
||||||
|
invalid={errors.languageTextMenuTr && touched.languageTextMenuTr}
|
||||||
|
errorMessage={errors.languageTextMenuTr}
|
||||||
|
>
|
||||||
|
<Field type="text" autoComplete="off" name="languageTextMenuTr" placeholder="Menu Text (Tr)" component={Input} />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
{/* Permission Group Name */}
|
||||||
|
<FormItem
|
||||||
|
label="Permission Group Name"
|
||||||
|
invalid={errors.permissionGroupName && touched.permissionGroupName}
|
||||||
|
errorMessage={errors.permissionGroupName}
|
||||||
|
asterisk={true}
|
||||||
|
>
|
||||||
|
<Field type="text" autoComplete="off" name="permissionGroupName">
|
||||||
|
{({ field, form }: FieldProps<string>) => (
|
||||||
|
<Select
|
||||||
|
componentAs={CreatableSelect}
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
placeholder="Permission Group Name"
|
||||||
|
isClearable={true}
|
||||||
|
isLoading={isLoadingPermissionGroup}
|
||||||
|
options={permissionGroupList}
|
||||||
|
value={
|
||||||
|
values.permissionGroupName
|
||||||
|
? (permissionGroupList?.find((o) => o.value === values.permissionGroupName) ?? {
|
||||||
|
label: values.permissionGroupName,
|
||||||
|
value: values.permissionGroupName,
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<Button block className="mt-4" variant="solid" type="button" onClick={onNext}>
|
||||||
|
{translate('::Next') || 'Next'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WizardStep1
|
||||||
Loading…
Reference in a new issue