Wizard Step2 güncellemesi
This commit is contained in:
parent
078ba898bd
commit
4c5cfe06f8
4 changed files with 1079 additions and 505 deletions
|
|
@ -1416,6 +1416,18 @@
|
||||||
"en": "List Form Settings",
|
"en": "List Form Settings",
|
||||||
"tr": "Liste Formu Ayarları"
|
"tr": "Liste Formu Ayarları"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "ListForms.Wizard.ListFormFields",
|
||||||
|
"en": "List Form Fields",
|
||||||
|
"tr": "Liste Formu Alanları"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "ListForms.Wizard.Deploy",
|
||||||
|
"en": "Deploy",
|
||||||
|
"tr": "Dağıtım"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Listforms.DataSource",
|
"key": "App.Listforms.DataSource",
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,20 @@
|
||||||
import Container from '@/components/shared/Container'
|
import { Button, FormContainer, Notification, Steps, toast } from '@/components/ui'
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
FormContainer,
|
|
||||||
FormItem,
|
|
||||||
Input,
|
|
||||||
Notification,
|
|
||||||
Select,
|
|
||||||
Steps,
|
|
||||||
toast,
|
|
||||||
} from '@/components/ui'
|
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
|
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
|
||||||
import { SelectBoxOption } from '@/types/shared'
|
import { SelectBoxOption } from '@/types/shared'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Field, FieldProps, Form, Formik, FormikProps } from 'formik'
|
import { Form, Formik, FormikProps } from 'formik'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import CreatableSelect from 'react-select/creatable'
|
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { dbSourceTypeOptions, selectCommandTypeOptions } from './edit/options'
|
|
||||||
import { getMenus } from '@/services/menu.service'
|
import { getMenus } from '@/services/menu.service'
|
||||||
import { getPermissions } from '@/services/identity.service'
|
import { getPermissions } from '@/services/identity.service'
|
||||||
import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
|
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 { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||||
|
import type { SqlObjectExplorerDto, DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
import { MenuItem } from '@/proxy/menus/menu'
|
import { MenuItem } from '@/proxy/menus/menu'
|
||||||
import WizardStep1, {
|
import WizardStep1, {
|
||||||
|
|
@ -33,10 +23,10 @@ import WizardStep1, {
|
||||||
filterNonLinkNodes,
|
filterNonLinkNodes,
|
||||||
findRootCode,
|
findRootCode,
|
||||||
} from './WizardStep1'
|
} from './WizardStep1'
|
||||||
|
import WizardStep2, { sqlDataTypeToDbType } from './WizardStep2'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
// ─── Formik initial values & validation ──────────────────────────────────────
|
// ─── Formik initial values & validation ──────────────────────────────────────
|
||||||
|
|
||||||
const initialValues: ListFormWizardDto = {
|
const initialValues: ListFormWizardDto = {
|
||||||
listFormCode: '',
|
listFormCode: '',
|
||||||
menuCode: '',
|
menuCode: '',
|
||||||
|
|
@ -73,7 +63,7 @@ const step2ValidationSchema = Yup.object().shape({
|
||||||
languageTextTitleTr: Yup.string(),
|
languageTextTitleTr: Yup.string(),
|
||||||
languageTextDescEn: Yup.string(),
|
languageTextDescEn: Yup.string(),
|
||||||
languageTextDescTr: Yup.string(),
|
languageTextDescTr: Yup.string(),
|
||||||
dataSourceCode: Yup.string(),
|
dataSourceCode: Yup.string().required(),
|
||||||
dataSourceConnectionString: Yup.string(),
|
dataSourceConnectionString: Yup.string(),
|
||||||
selectCommandType: Yup.string().required(),
|
selectCommandType: Yup.string().required(),
|
||||||
selectCommand: Yup.string().required(),
|
selectCommand: Yup.string().required(),
|
||||||
|
|
@ -93,6 +83,73 @@ const Wizard = () => {
|
||||||
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)
|
||||||
|
|
||||||
|
// ── DB Objects (Tables / SP / Views / Functions) ──
|
||||||
|
const [dbObjects, setDbObjects] = useState<SqlObjectExplorerDto | null>(null)
|
||||||
|
const [isLoadingDbObjects, setIsLoadingDbObjects] = useState(false)
|
||||||
|
const [currentDataSource, setCurrentDataSource] = useState('')
|
||||||
|
|
||||||
|
const loadDbObjects = async (dsCode: string) => {
|
||||||
|
if (!dsCode) {
|
||||||
|
setDbObjects(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsLoadingDbObjects(true)
|
||||||
|
try {
|
||||||
|
const res = await sqlObjectManagerService.getAllObjects(dsCode)
|
||||||
|
setDbObjects(res.data)
|
||||||
|
} catch {
|
||||||
|
setDbObjects(null)
|
||||||
|
} finally {
|
||||||
|
setIsLoadingDbObjects(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadDbObjects(currentDataSource)
|
||||||
|
}, [currentDataSource])
|
||||||
|
|
||||||
|
// ── Column List for KeyFieldName & Column Selector ──
|
||||||
|
const [selectCommandColumns, setSelectCommandColumns] = useState<DatabaseColumnDto[]>([])
|
||||||
|
const [isLoadingColumns, setIsLoadingColumns] = useState(false)
|
||||||
|
const [selectedColumns, setSelectedColumns] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
|
const loadColumns = async (dsCode: string, schema: string, name: string) => {
|
||||||
|
if (!dsCode || !name) {
|
||||||
|
setSelectCommandColumns([])
|
||||||
|
setSelectedColumns(new Set())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsLoadingColumns(true)
|
||||||
|
try {
|
||||||
|
const res = await sqlObjectManagerService.getTableColumns(dsCode, schema, name)
|
||||||
|
const cols = res.data ?? []
|
||||||
|
setSelectCommandColumns(cols)
|
||||||
|
setSelectedColumns(new Set(cols.map((c) => c.columnName)))
|
||||||
|
// Auto-select first column as key field
|
||||||
|
if (cols.length > 0) {
|
||||||
|
const first = cols[0]
|
||||||
|
formikRef.current?.setFieldValue('keyFieldName', first.columnName)
|
||||||
|
formikRef.current?.setFieldValue('keyFieldDbSourceType', sqlDataTypeToDbType(first.dataType))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setSelectCommandColumns([])
|
||||||
|
setSelectedColumns(new Set())
|
||||||
|
} finally {
|
||||||
|
setIsLoadingColumns(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleColumn = (col: string) =>
|
||||||
|
setSelectedColumns((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
next.has(col) ? next.delete(col) : next.add(col)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleAllColumns = (all: boolean) =>
|
||||||
|
setSelectedColumns(all ? new Set(selectCommandColumns.map((c) => c.columnName)) : new Set())
|
||||||
|
|
||||||
const getDataSourceList = async () => {
|
const getDataSourceList = async () => {
|
||||||
setIsLoadingDataSource(true)
|
setIsLoadingDataSource(true)
|
||||||
const response = await getDataSources()
|
const response = await getDataSources()
|
||||||
|
|
@ -212,6 +269,20 @@ const Wizard = () => {
|
||||||
|
|
||||||
const handleBack = () => setCurrentStep(0)
|
const handleBack = () => setCurrentStep(0)
|
||||||
|
|
||||||
|
const handleNext2 = async () => {
|
||||||
|
if (!formikRef.current) return
|
||||||
|
const errors = await formikRef.current.validateForm()
|
||||||
|
const step2Fields = Object.keys(step2ValidationSchema.fields)
|
||||||
|
const hasStep2Errors = step2Fields.some((f) => errors[f as keyof ListFormWizardDto])
|
||||||
|
const touchedStep2 = step2Fields.reduce(
|
||||||
|
(acc, key) => ({ ...acc, [key]: true }),
|
||||||
|
{} as Record<string, boolean>,
|
||||||
|
)
|
||||||
|
await formikRef.current.setTouched({ ...formikRef.current.touched, ...touchedStep2 })
|
||||||
|
if (hasStep2Errors) return
|
||||||
|
setCurrentStep(2)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Helmet
|
<Helmet
|
||||||
|
|
@ -226,294 +297,123 @@ const Wizard = () => {
|
||||||
<Steps.Item
|
<Steps.Item
|
||||||
title={translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings'}
|
title={translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings'}
|
||||||
/>
|
/>
|
||||||
|
<Steps.Item
|
||||||
|
title={translate('::ListForms.Wizard.ListFormFields') || 'List Form Fields'}
|
||||||
|
/>
|
||||||
|
<Steps.Item title={translate('::ListForms.Wizard.Deploy') || 'Deploy'} />
|
||||||
</Steps>
|
</Steps>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 xl:grid-cols-3">
|
<Formik
|
||||||
<Formik
|
innerRef={formikRef}
|
||||||
innerRef={formikRef}
|
initialValues={{ ...initialValues }}
|
||||||
initialValues={{ ...initialValues }}
|
validationSchema={listFormValidationSchema}
|
||||||
validationSchema={listFormValidationSchema}
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
setSubmitting(true)
|
||||||
setSubmitting(true)
|
try {
|
||||||
try {
|
await postListFormWizard({ ...values })
|
||||||
await postListFormWizard({ ...values })
|
toast.push(
|
||||||
toast.push(
|
<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)
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate(
|
||||||
|
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(
|
||||||
|
':listFormCode',
|
||||||
|
values.listFormCode,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
setSubmitting(false)
|
}, 500)
|
||||||
setTimeout(() => {
|
} catch (error: any) {
|
||||||
navigate(
|
toast.push(<Notification title={error.message} type="danger" />, {
|
||||||
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(
|
placement: 'top-end',
|
||||||
':listFormCode',
|
})
|
||||||
values.listFormCode,
|
}
|
||||||
),
|
}}
|
||||||
)
|
>
|
||||||
}, 500)
|
{({ touched, errors, isSubmitting, values }) => (
|
||||||
} catch (error: any) {
|
<Form>
|
||||||
toast.push(<Notification title={error.message} type="danger" />, {
|
<FormContainer size="sm">
|
||||||
placement: 'top-end',
|
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
||||||
})
|
{currentStep === 0 && (
|
||||||
}
|
<WizardStep1
|
||||||
}}
|
values={values}
|
||||||
>
|
errors={errors}
|
||||||
{({ touched, errors, isSubmitting, values }) => (
|
touched={touched}
|
||||||
<Form>
|
wizardName={wizardName}
|
||||||
<FormContainer size="sm">
|
onWizardNameChange={handleWizardNameChange}
|
||||||
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
rawMenuItems={rawMenuItems}
|
||||||
{currentStep === 0 && (
|
menuTree={menuTree}
|
||||||
<WizardStep1
|
isLoadingMenu={isLoadingMenu}
|
||||||
values={values}
|
onMenuParentChange={handleMenuParentChange}
|
||||||
errors={errors}
|
onClearMenuParent={() => formikRef.current?.setFieldValue('menuParentCode', '')}
|
||||||
touched={touched}
|
onReloadMenu={getMenuList}
|
||||||
wizardName={wizardName}
|
permissionGroupList={permissionGroupList}
|
||||||
onWizardNameChange={handleWizardNameChange}
|
isLoadingPermissionGroup={isLoadingPermissionGroup}
|
||||||
rawMenuItems={rawMenuItems}
|
onNext={handleNext}
|
||||||
menuTree={menuTree}
|
translate={translate}
|
||||||
isLoadingMenu={isLoadingMenu}
|
/>
|
||||||
onMenuParentChange={handleMenuParentChange}
|
)}
|
||||||
onClearMenuParent={() => formikRef.current?.setFieldValue('menuParentCode', '')}
|
|
||||||
onReloadMenu={getMenuList}
|
|
||||||
permissionGroupList={permissionGroupList}
|
|
||||||
isLoadingPermissionGroup={isLoadingPermissionGroup}
|
|
||||||
onNext={handleNext}
|
|
||||||
translate={translate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ─── Step 2: Data Settings ───────────────────────────── */}
|
{/* ─── Step 2: Data Settings ───────────────────────────── */}
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<>
|
<WizardStep2
|
||||||
{/* ListForm Code */}
|
values={values}
|
||||||
<FormItem
|
errors={errors}
|
||||||
label="ListForm Code"
|
touched={touched}
|
||||||
invalid={!!(errors.listFormCode && touched.listFormCode)}
|
isLoadingDataSource={isLoadingDataSource}
|
||||||
errorMessage={errors.listFormCode}
|
dataSourceList={dataSourceList}
|
||||||
asterisk={true}
|
isDataSourceNew={isDataSourceNew}
|
||||||
extra={
|
onDataSourceSelect={setCurrentDataSource}
|
||||||
<span className="text-xs text-gray-400">
|
onDataSourceNewChange={setIsDataSourceNew}
|
||||||
Auto-derived from Wizard Name, editable
|
dbObjects={dbObjects}
|
||||||
</span>
|
isLoadingDbObjects={isLoadingDbObjects}
|
||||||
}
|
selectCommandColumns={selectCommandColumns}
|
||||||
>
|
isLoadingColumns={isLoadingColumns}
|
||||||
<Field
|
selectedColumns={selectedColumns}
|
||||||
type="text"
|
onLoadColumns={loadColumns}
|
||||||
autoComplete="off"
|
onClearColumns={() => {
|
||||||
name="listFormCode"
|
setSelectCommandColumns([])
|
||||||
placeholder="e.g. App.Soutes"
|
setSelectedColumns(new Set())
|
||||||
component={Input}
|
}}
|
||||||
/>
|
onToggleColumn={toggleColumn}
|
||||||
</FormItem>
|
onToggleAllColumns={toggleAllColumns}
|
||||||
|
translate={translate}
|
||||||
|
onBack={handleBack}
|
||||||
|
onNext={handleNext2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
{/* ─── Step 3: List Form Fields ───────────────────────────── */}
|
||||||
<FormItem
|
{currentStep === 2 && (
|
||||||
label="Title (En)"
|
<div className="flex gap-2 mt-4">
|
||||||
invalid={errors.languageTextTitleEn && touched.languageTextTitleEn}
|
<Button block variant="default" type="button" onClick={() => setCurrentStep(1)}>
|
||||||
errorMessage={errors.languageTextTitleEn}
|
{translate('::Back') || 'Back'}
|
||||||
>
|
</Button>
|
||||||
<Field
|
<Button block variant="solid" type="button" onClick={() => setCurrentStep(3)}>
|
||||||
type="text"
|
{translate('::Next') || 'Next'}
|
||||||
autoComplete="off"
|
</Button>
|
||||||
name="languageTextTitleEn"
|
</div>
|
||||||
placeholder="Title (En)"
|
)}
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label="Title (Tr)"
|
|
||||||
invalid={errors.languageTextTitleTr && touched.languageTextTitleTr}
|
|
||||||
errorMessage={errors.languageTextTitleTr}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="languageTextTitleTr"
|
|
||||||
placeholder="Title (Tr)"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
{/* ─── Step 4: Deploy ───────────────────────────── */}
|
||||||
<FormItem
|
{currentStep === 3 && (
|
||||||
label="Description (En)"
|
<div className="flex gap-2 mt-4">
|
||||||
invalid={errors.languageTextDescEn && touched.languageTextDescEn}
|
<Button block variant="default" type="button" onClick={() => setCurrentStep(2)}>
|
||||||
errorMessage={errors.languageTextDescEn}
|
{translate('::Back') || 'Back'}
|
||||||
>
|
</Button>
|
||||||
<Field
|
<Button block variant="solid" loading={isSubmitting} type="submit">
|
||||||
type="text"
|
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
|
||||||
autoComplete="off"
|
</Button>
|
||||||
name="languageTextDescEn"
|
</div>
|
||||||
placeholder="Description (En)"
|
)}
|
||||||
component={Input}
|
</FormContainer>
|
||||||
/>
|
</Form>
|
||||||
</FormItem>
|
)}
|
||||||
<FormItem
|
</Formik>
|
||||||
label="Description (Tr)"
|
|
||||||
invalid={errors.languageTextDescTr && touched.languageTextDescTr}
|
|
||||||
errorMessage={errors.languageTextDescTr}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="languageTextDescTr"
|
|
||||||
placeholder="Description (Tr)"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<FormItem
|
|
||||||
label="Data Source Code"
|
|
||||||
invalid={errors.dataSourceCode && touched.dataSourceCode}
|
|
||||||
errorMessage={errors.dataSourceCode}
|
|
||||||
>
|
|
||||||
<Field type="text" autoComplete="off" name="dataSourceCode">
|
|
||||||
{({ field, form }: FieldProps<string>) => (
|
|
||||||
<Select
|
|
||||||
componentAs={CreatableSelect}
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
placeholder="Data Source Code"
|
|
||||||
isClearable={true}
|
|
||||||
isLoading={isLoadingDataSource}
|
|
||||||
options={dataSourceList}
|
|
||||||
value={
|
|
||||||
values.dataSourceCode
|
|
||||||
? (dataSourceList?.find(
|
|
||||||
(o) => o.value === values.dataSourceCode,
|
|
||||||
) ?? {
|
|
||||||
label: values.dataSourceCode,
|
|
||||||
value: values.dataSourceCode,
|
|
||||||
})
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
onChange={(option) => {
|
|
||||||
form.setFieldValue(field.name, option?.value)
|
|
||||||
setIsDataSourceNew(
|
|
||||||
!!option?.value &&
|
|
||||||
!dataSourceList.some((a) => a.value === option?.value),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
{isDataSourceNew && (
|
|
||||||
<FormItem
|
|
||||||
label="Connection String"
|
|
||||||
invalid={
|
|
||||||
errors.dataSourceConnectionString && touched.dataSourceConnectionString
|
|
||||||
}
|
|
||||||
errorMessage={errors.dataSourceConnectionString}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="dataSourceConnectionString"
|
|
||||||
placeholder="Connection String"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<FormItem
|
|
||||||
label="Select Command Type"
|
|
||||||
invalid={errors.selectCommandType && touched.selectCommandType}
|
|
||||||
errorMessage={errors.selectCommandType}
|
|
||||||
asterisk={true}
|
|
||||||
>
|
|
||||||
<Field type="text" autoComplete="off" name="selectCommandType">
|
|
||||||
{({ field, form }: FieldProps<SelectCommandTypeEnum>) => (
|
|
||||||
<Select
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
options={selectCommandTypeOptions}
|
|
||||||
value={selectCommandTypeOptions.find(
|
|
||||||
(o: any) => o.value === field.value,
|
|
||||||
)}
|
|
||||||
onChange={(o) => form.setFieldValue(field.name, o?.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Select Command"
|
|
||||||
invalid={errors.selectCommand && touched.selectCommand}
|
|
||||||
errorMessage={errors.selectCommand}
|
|
||||||
asterisk={true}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="selectCommand"
|
|
||||||
placeholder="Select Command"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<FormItem
|
|
||||||
label="Key Field Name"
|
|
||||||
invalid={errors.keyFieldName && touched.keyFieldName}
|
|
||||||
errorMessage={errors.keyFieldName}
|
|
||||||
asterisk={true}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
name="keyFieldName"
|
|
||||||
placeholder="Key Field Name"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Key Field Db Source Type"
|
|
||||||
invalid={errors.keyFieldDbSourceType && touched.keyFieldDbSourceType}
|
|
||||||
errorMessage={errors.keyFieldDbSourceType}
|
|
||||||
asterisk={true}
|
|
||||||
>
|
|
||||||
<Field type="text" autoComplete="off" name="keyFieldDbSourceType">
|
|
||||||
{({ field, form }: FieldProps<DbTypeEnum>) => (
|
|
||||||
<Select
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
options={dbSourceTypeOptions}
|
|
||||||
value={dbSourceTypeOptions?.filter(
|
|
||||||
(o: any) => o.value === values.keyFieldDbSourceType,
|
|
||||||
)}
|
|
||||||
onChange={(o) => form.setFieldValue(field.name, o?.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 mt-4">
|
|
||||||
<Button block variant="default" type="button" onClick={handleBack}>
|
|
||||||
{translate('::Back') || 'Back'}
|
|
||||||
</Button>
|
|
||||||
<Button block variant="solid" loading={isSubmitting} type="submit">
|
|
||||||
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</FormContainer>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,4 @@
|
||||||
import {
|
import { Button, Dialog, FormItem, Input, Notification, Select, toast } from '@/components/ui'
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
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'
|
||||||
|
|
@ -102,9 +94,20 @@ interface TreeNodeProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function TreeNode({
|
function TreeNode({
|
||||||
node, depth, selectedCode, onSelect, expanded, onToggle,
|
node,
|
||||||
editingCode, editingValue, saving,
|
depth,
|
||||||
onStartEdit, onEditChange, onSaveEdit, onCancelEdit, onDelete,
|
selectedCode,
|
||||||
|
onSelect,
|
||||||
|
expanded,
|
||||||
|
onToggle,
|
||||||
|
editingCode,
|
||||||
|
editingValue,
|
||||||
|
saving,
|
||||||
|
onStartEdit,
|
||||||
|
onEditChange,
|
||||||
|
onSaveEdit,
|
||||||
|
onCancelEdit,
|
||||||
|
onDelete,
|
||||||
}: TreeNodeProps) {
|
}: TreeNodeProps) {
|
||||||
const hasChildren = node.children.length > 0
|
const hasChildren = node.children.length > 0
|
||||||
const isExpanded = expanded.has(node.code)
|
const isExpanded = expanded.has(node.code)
|
||||||
|
|
@ -125,17 +128,24 @@ function TreeNode({
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="w-4 h-4 flex items-center justify-center shrink-0 text-xs cursor-pointer"
|
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) }}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (hasChildren) onToggle(node.code)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{hasChildren ? (
|
{hasChildren ? (
|
||||||
isExpanded
|
isExpanded ? (
|
||||||
? <FaChevronDown className="text-gray-400" />
|
<FaChevronDown className="text-gray-400" />
|
||||||
: <FaChevronRight className="text-gray-400" />
|
) : (
|
||||||
|
<FaChevronRight className="text-gray-400" />
|
||||||
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{NodeIcon && (
|
{NodeIcon && (
|
||||||
<NodeIcon className={`text-base shrink-0 ${isSelected ? 'text-indigo-200' : 'text-gray-400'}`} />
|
<NodeIcon
|
||||||
|
className={`text-base shrink-0 ${isSelected ? 'text-indigo-200' : 'text-gray-400'}`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
|
|
@ -151,7 +161,10 @@ function TreeNode({
|
||||||
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"
|
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)}>
|
<span
|
||||||
|
className="flex-1 text-sm truncate cursor-pointer"
|
||||||
|
onClick={() => onSelect(node.code)}
|
||||||
|
>
|
||||||
{node.displayName}
|
{node.displayName}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -165,52 +178,76 @@ function TreeNode({
|
||||||
|
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<>
|
<>
|
||||||
<button type="button" disabled={saving}
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); onSaveEdit(node) }}
|
type="button"
|
||||||
className="p-1 text-green-500 hover:text-green-600 disabled:opacity-40">
|
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" />
|
<FaCheck className="text-xs" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); onCancelEdit() }}
|
type="button"
|
||||||
className="p-1 text-gray-400 hover:text-gray-600">
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onCancelEdit()
|
||||||
|
}}
|
||||||
|
className="p-1 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
<FaTimes className="text-xs" />
|
<FaTimes className="text-xs" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="shrink-0 flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
<span className="shrink-0 flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button type="button" title="Rename"
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); onStartEdit(node.code, node.displayName) }}
|
type="button"
|
||||||
className={`p-1 ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-indigo-500'}`}>
|
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" />
|
<FaEdit className="text-xs" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" title="Delete"
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); onDelete(node) }}
|
type="button"
|
||||||
className={`p-1 ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-red-500'}`}>
|
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" />
|
<FaTrash className="text-xs" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && node.children.map((child) => (
|
{isExpanded &&
|
||||||
<TreeNode
|
node.children.map((child) => (
|
||||||
key={child.code}
|
<TreeNode
|
||||||
node={child as MenuTreeNode & { id?: string }}
|
key={child.code}
|
||||||
depth={depth + 1}
|
node={child as MenuTreeNode & { id?: string }}
|
||||||
selectedCode={selectedCode}
|
depth={depth + 1}
|
||||||
onSelect={onSelect}
|
selectedCode={selectedCode}
|
||||||
expanded={expanded}
|
onSelect={onSelect}
|
||||||
onToggle={onToggle}
|
expanded={expanded}
|
||||||
editingCode={editingCode}
|
onToggle={onToggle}
|
||||||
editingValue={editingValue}
|
editingCode={editingCode}
|
||||||
saving={saving}
|
editingValue={editingValue}
|
||||||
onStartEdit={onStartEdit}
|
saving={saving}
|
||||||
onEditChange={onEditChange}
|
onStartEdit={onStartEdit}
|
||||||
onSaveEdit={onSaveEdit}
|
onEditChange={onEditChange}
|
||||||
onCancelEdit={onCancelEdit}
|
onSaveEdit={onSaveEdit}
|
||||||
onDelete={onDelete}
|
onCancelEdit={onCancelEdit}
|
||||||
/>
|
onDelete={onDelete}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -227,14 +264,26 @@ interface MenuTreeInlineProps {
|
||||||
onReload: () => void
|
onReload: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuTreeInline({ value, onChange, nodes, rawItems, isLoading, invalid, onReload }: MenuTreeInlineProps) {
|
function MenuTreeInline({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
nodes,
|
||||||
|
rawItems,
|
||||||
|
isLoading,
|
||||||
|
invalid,
|
||||||
|
onReload,
|
||||||
|
}: MenuTreeInlineProps) {
|
||||||
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
||||||
const [editingCode, setEditingCode] = useState<string | null>(null)
|
const [editingCode, setEditingCode] = useState<string | null>(null)
|
||||||
const [editingValue, setEditingValue] = useState('')
|
const [editingValue, setEditingValue] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
const toggle = (code: string) =>
|
const toggle = (code: string) =>
|
||||||
setExpanded((prev) => { const n = new Set(prev); n.has(code) ? n.delete(code) : n.add(code); return n })
|
setExpanded((prev) => {
|
||||||
|
const n = new Set(prev)
|
||||||
|
n.has(code) ? n.delete(code) : n.add(code)
|
||||||
|
return n
|
||||||
|
})
|
||||||
|
|
||||||
const handleStartEdit = (code: string, currentName: string) => {
|
const handleStartEdit = (code: string, currentName: string) => {
|
||||||
setEditingCode(code)
|
setEditingCode(code)
|
||||||
|
|
@ -247,7 +296,10 @@ function MenuTreeInline({ value, onChange, nodes, rawItems, isLoading, invalid,
|
||||||
try {
|
try {
|
||||||
const original = rawItems.find((i) => i.code === node.code)
|
const original = rawItems.find((i) => i.code === node.code)
|
||||||
if (!original) return
|
if (!original) return
|
||||||
await menuService.update(node.id, { ...original, displayName: editingValue.trim() } as MenuDto)
|
await menuService.update(node.id, {
|
||||||
|
...original,
|
||||||
|
displayName: editingValue.trim(),
|
||||||
|
} as MenuDto)
|
||||||
onReload()
|
onReload()
|
||||||
setEditingCode(null)
|
setEditingCode(null)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
@ -279,23 +331,37 @@ function MenuTreeInline({ value, onChange, nodes, rawItems, isLoading, invalid,
|
||||||
const enrichedNodes = nodes.map(enrichNode)
|
const enrichedNodes = nodes.map(enrichNode)
|
||||||
|
|
||||||
const sharedNodeProps = {
|
const sharedNodeProps = {
|
||||||
expanded, onToggle: toggle,
|
expanded,
|
||||||
editingCode, editingValue, saving,
|
onToggle: toggle,
|
||||||
onStartEdit: handleStartEdit, onEditChange: setEditingValue,
|
editingCode,
|
||||||
onSaveEdit: handleSaveEdit, onCancelEdit: () => setEditingCode(null),
|
editingValue,
|
||||||
|
saving,
|
||||||
|
onStartEdit: handleStartEdit,
|
||||||
|
onEditChange: setEditingValue,
|
||||||
|
onSaveEdit: handleSaveEdit,
|
||||||
|
onCancelEdit: () => setEditingCode(null),
|
||||||
onDelete: handleDelete,
|
onDelete: handleDelete,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
||||||
<div className="h-48 overflow-y-auto py-1">
|
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-72 overflow-y-auto py-1">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="px-4 py-3 text-sm text-gray-400">Loading…</div>
|
<div className="px-4 py-3 text-sm text-gray-400">Loading…</div>
|
||||||
) : enrichedNodes.length === 0 ? (
|
) : enrichedNodes.length === 0 ? (
|
||||||
<div className="px-4 py-3 text-sm text-gray-400">No menus available</div>
|
<div className="px-4 py-3 text-sm text-gray-400">No menus available</div>
|
||||||
) : (
|
) : (
|
||||||
enrichedNodes.map((node) => (
|
enrichedNodes.map((node) => (
|
||||||
<TreeNode key={node.code} node={node} depth={0} selectedCode={value} onSelect={onChange} {...sharedNodeProps} />
|
<TreeNode
|
||||||
|
key={node.code}
|
||||||
|
node={node}
|
||||||
|
depth={0}
|
||||||
|
selectedCode={value}
|
||||||
|
onSelect={onChange}
|
||||||
|
{...sharedNodeProps}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -327,7 +393,9 @@ function IconPickerField({ value, onChange, invalid }: IconPickerFieldProps) {
|
||||||
const displayed = filtered.slice(0, limit)
|
const displayed = filtered.slice(0, limit)
|
||||||
const hasMore = displayed.length < filtered.length
|
const hasMore = displayed.length < filtered.length
|
||||||
|
|
||||||
useEffect(() => { setLimit(ICON_PAGE_SIZE) }, [search])
|
useEffect(() => {
|
||||||
|
setLimit(ICON_PAGE_SIZE)
|
||||||
|
}, [search])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(e: MouseEvent) {
|
function handleClickOutside(e: MouseEvent) {
|
||||||
|
|
@ -354,7 +422,9 @@ function IconPickerField({ value, onChange, invalid }: IconPickerFieldProps) {
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400 flex-1">Select icon...</span>
|
<span className="text-gray-400 flex-1">Select icon...</span>
|
||||||
)}
|
)}
|
||||||
<FaChevronDown className={`text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`} />
|
<FaChevronDown
|
||||||
|
className={`text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && (
|
{open && (
|
||||||
|
|
@ -375,7 +445,11 @@ function IconPickerField({ value, onChange, invalid }: IconPickerFieldProps) {
|
||||||
key={key}
|
key={key}
|
||||||
type="button"
|
type="button"
|
||||||
title={key}
|
title={key}
|
||||||
onClick={() => { onChange(key); setOpen(false); setSearch('') }}
|
onClick={() => {
|
||||||
|
onChange(key)
|
||||||
|
setOpen(false)
|
||||||
|
setSearch('')
|
||||||
|
}}
|
||||||
className={`flex items-center justify-center h-10 w-full rounded text-xl transition-colors
|
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'}`}
|
${value === key ? 'bg-indigo-500 text-white' : 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300'}`}
|
||||||
>
|
>
|
||||||
|
|
@ -385,7 +459,9 @@ function IconPickerField({ value, onChange, invalid }: IconPickerFieldProps) {
|
||||||
</div>
|
</div>
|
||||||
{hasMore && (
|
{hasMore && (
|
||||||
<div className="px-3 py-1.5 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between">
|
<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>
|
<span className="text-xs text-gray-400">
|
||||||
|
{displayed.length} / {filtered.length}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setLimit((l) => l + ICON_PAGE_SIZE)}
|
onClick={() => setLimit((l) => l + ICON_PAGE_SIZE)}
|
||||||
|
|
@ -412,12 +488,34 @@ interface MenuAddDialogProps {
|
||||||
onSaved: () => void
|
onSaved: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuAddDialog({ isOpen, onClose, initialParentCode, initialOrder, rawItems, onSaved }: MenuAddDialogProps) {
|
function MenuAddDialog({
|
||||||
const [form, setForm] = useState({ code: '', displayName: '', parentCode: initialParentCode, icon: '', shortName: '', order: initialOrder })
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
initialParentCode,
|
||||||
|
initialOrder,
|
||||||
|
rawItems,
|
||||||
|
onSaved,
|
||||||
|
}: MenuAddDialogProps) {
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
code: '',
|
||||||
|
displayName: '',
|
||||||
|
parentCode: initialParentCode,
|
||||||
|
icon: '',
|
||||||
|
shortName: '',
|
||||||
|
order: initialOrder,
|
||||||
|
})
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) setForm({ code: '', displayName: '', parentCode: initialParentCode, icon: '', shortName: '', order: initialOrder })
|
if (isOpen)
|
||||||
|
setForm({
|
||||||
|
code: '',
|
||||||
|
displayName: '',
|
||||||
|
parentCode: initialParentCode,
|
||||||
|
icon: '',
|
||||||
|
shortName: '',
|
||||||
|
order: initialOrder,
|
||||||
|
})
|
||||||
}, [isOpen, initialParentCode, initialOrder])
|
}, [isOpen, initialParentCode, initialOrder])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
|
@ -451,38 +549,80 @@ function MenuAddDialog({ isOpen, onClose, initialParentCode, initialOrder, rawIt
|
||||||
<h5 className="text-base font-semibold text-gray-800 dark:text-gray-100">Yeni Menü Ekle</h5>
|
<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-3">
|
||||||
<div className="flex flex-col gap-1">
|
<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>
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||||
<input value={form.code} onChange={(e) => setForm((p) => ({ ...p, code: e.target.value }))} placeholder="App.MyMenu"
|
Code <span className="text-red-500">*</span>
|
||||||
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" />
|
</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>
|
||||||
<div className="flex flex-col gap-1">
|
<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>
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||||
<input value={form.displayName} onChange={(e) => setForm((p) => ({ ...p, displayName: e.target.value }))} placeholder="My Menu"
|
Display Name <span className="text-red-500">*</span>
|
||||||
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" />
|
</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>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">Icon</label>
|
<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 }))} />
|
<IconPickerField
|
||||||
|
value={form.icon}
|
||||||
|
onChange={(key) => setForm((p) => ({ ...p, icon: key }))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">Menu Parent</label>
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||||
<input disabled value={form.parentCode || '(Ana Menü)'}
|
Menu Parent
|
||||||
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" />
|
</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>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">Sıra (Order)</label>
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||||
<input type="number" value={form.order} onChange={(e) => setForm((p) => ({ ...p, order: Number(e.target.value) }))}
|
Sıra (Order)
|
||||||
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" />
|
</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>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">Short Name</label>
|
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||||
<input value={form.shortName} onChange={(e) => setForm((p) => ({ ...p, shortName: e.target.value }))} placeholder="My Menu (short)"
|
Short Name
|
||||||
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" />
|
</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>
|
</div>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<Button size="sm" variant="plain" onClick={onClose}>İptal</Button>
|
<Button size="sm" variant="plain" onClick={onClose}>
|
||||||
<Button size="sm" variant="solid" loading={saving} disabled={!form.code.trim() || !form.displayName.trim()} onClick={handleSave}>Kaydet</Button>
|
İptal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="solid"
|
||||||
|
loading={saving}
|
||||||
|
disabled={!form.code.trim() || !form.displayName.trim()}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Kaydet
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
@ -510,152 +650,212 @@ export interface WizardStep1Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const WizardStep1 = ({
|
const WizardStep1 = ({
|
||||||
values, errors, touched,
|
values,
|
||||||
wizardName, onWizardNameChange,
|
errors,
|
||||||
rawMenuItems, menuTree, isLoadingMenu,
|
touched,
|
||||||
onMenuParentChange, onClearMenuParent, onReloadMenu,
|
wizardName,
|
||||||
permissionGroupList, isLoadingPermissionGroup,
|
onWizardNameChange,
|
||||||
onNext, translate,
|
rawMenuItems,
|
||||||
|
menuTree,
|
||||||
|
isLoadingMenu,
|
||||||
|
onMenuParentChange,
|
||||||
|
onClearMenuParent,
|
||||||
|
onReloadMenu,
|
||||||
|
permissionGroupList,
|
||||||
|
isLoadingPermissionGroup,
|
||||||
|
onNext,
|
||||||
|
translate,
|
||||||
}: WizardStep1Props) => {
|
}: WizardStep1Props) => {
|
||||||
const [menuDialogOpen, setMenuDialogOpen] = useState(false)
|
const [menuDialogOpen, setMenuDialogOpen] = useState(false)
|
||||||
const [menuDialogParentCode, setMenuDialogParentCode] = useState('')
|
const [menuDialogParentCode, setMenuDialogParentCode] = useState('')
|
||||||
const [menuDialogInitialOrder, setMenuDialogInitialOrder] = useState(999)
|
const [menuDialogInitialOrder, setMenuDialogInitialOrder] = useState(999)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="pb-20">
|
||||||
{/* Wizard Name */}
|
{/* Wizard Name */}
|
||||||
<FormItem
|
<FormItem
|
||||||
label="Wizard Name"
|
label="Wizard Name"
|
||||||
asterisk={true}
|
asterisk={true}
|
||||||
extra={<span className="text-xs ml-2 text-gray-400">Used to generate ListForm Code and Menu Code</span>}
|
extra={
|
||||||
|
<span className="text-xs ml-2 text-gray-400">
|
||||||
|
Used to generate ListForm Code and Menu Code
|
||||||
|
</span>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder="e.g. Soutes"
|
placeholder="e.g. Routes, Products, Orders"
|
||||||
value={wizardName}
|
value={wizardName}
|
||||||
|
autoFocus
|
||||||
onChange={(e) => onWizardNameChange(e.target.value)}
|
onChange={(e) => onWizardNameChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
{/* Menu Parent */}
|
{/* Menu Code / Menu Text / Permission Group — 2-column grid */}
|
||||||
<FormItem
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
||||||
label="Menu Parent"
|
{/* Col 1 */}
|
||||||
invalid={errors.menuParentCode && touched.menuParentCode}
|
<div>
|
||||||
errorMessage={errors.menuParentCode}
|
{/* Menu Parent */}
|
||||||
asterisk={true}
|
<FormItem
|
||||||
extra={
|
label="Menu Parent"
|
||||||
<div className="flex items-center gap-2 ml-3">
|
invalid={errors.menuParentCode && touched.menuParentCode}
|
||||||
<button
|
errorMessage={errors.menuParentCode}
|
||||||
type="button"
|
asterisk={true}
|
||||||
onClick={() => {
|
extra={
|
||||||
setMenuDialogParentCode(values.menuParentCode ? findRootCode(rawMenuItems, values.menuParentCode) : '')
|
<div className="flex items-center gap-2 ml-3">
|
||||||
const selectedItem = rawMenuItems.find((i) => i.code === values.menuParentCode)
|
<button
|
||||||
setMenuDialogInitialOrder(selectedItem?.order ?? 999)
|
type="button"
|
||||||
setMenuDialogOpen(true)
|
onClick={() => {
|
||||||
}}
|
setMenuDialogParentCode(
|
||||||
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-green-500 text-white hover:bg-green-600"
|
values.menuParentCode
|
||||||
>
|
? findRootCode(rawMenuItems, values.menuParentCode)
|
||||||
<FaPlus className="text-xs" /> Ekle
|
: '',
|
||||||
</button>
|
)
|
||||||
{values.menuParentCode && (
|
const selectedItem = rawMenuItems.find((i) => i.code === values.menuParentCode)
|
||||||
<button
|
setMenuDialogInitialOrder(selectedItem?.order ?? 999)
|
||||||
type="button"
|
setMenuDialogOpen(true)
|
||||||
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"
|
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-green-500 text-white hover:bg-green-600"
|
||||||
>
|
>
|
||||||
<FaTimes className="text-xs" /> Seçimi Kaldır
|
<FaPlus className="text-xs" /> Ekle
|
||||||
</button>
|
</button>
|
||||||
)}
|
{values.menuParentCode && (
|
||||||
</div>
|
<button
|
||||||
}
|
type="button"
|
||||||
>
|
onClick={(e) => {
|
||||||
<Field name="menuParentCode">
|
e.stopPropagation()
|
||||||
{() => (
|
e.preventDefault()
|
||||||
<MenuTreeInline
|
onClearMenuParent()
|
||||||
value={values.menuParentCode}
|
}}
|
||||||
onChange={onMenuParentChange}
|
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"
|
||||||
nodes={menuTree}
|
>
|
||||||
rawItems={rawMenuItems}
|
<FaTimes className="text-xs" /> Seçimi Kaldır
|
||||||
isLoading={isLoadingMenu}
|
</button>
|
||||||
invalid={!!(errors.menuParentCode && touched.menuParentCode)}
|
)}
|
||||||
onReload={onReloadMenu}
|
</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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Col 2 */}
|
||||||
|
<div>
|
||||||
|
{/* 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.Routes, App.Products, App.Orders"
|
||||||
|
component={Input}
|
||||||
/>
|
/>
|
||||||
)}
|
</FormItem>
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<MenuAddDialog
|
{/* Menu Text (En) */}
|
||||||
isOpen={menuDialogOpen}
|
<FormItem
|
||||||
onClose={() => setMenuDialogOpen(false)}
|
label="Menu Text (En)"
|
||||||
initialParentCode={menuDialogParentCode}
|
asterisk={true}
|
||||||
initialOrder={menuDialogInitialOrder}
|
invalid={!!(errors.languageTextMenuEn && touched.languageTextMenuEn)}
|
||||||
rawItems={rawMenuItems}
|
errorMessage={errors.languageTextMenuEn}
|
||||||
onSaved={onReloadMenu}
|
>
|
||||||
/>
|
<Field
|
||||||
|
type="text"
|
||||||
{/* Menu Code */}
|
autoComplete="off"
|
||||||
<FormItem
|
name="languageTextMenuEn"
|
||||||
label="Menu Code"
|
placeholder="English Menu Text"
|
||||||
invalid={!!(errors.menuCode && touched.menuCode)}
|
component={Input}
|
||||||
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)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</FormItem>
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<Button block className="mt-4" variant="solid" type="button" onClick={onNext}>
|
{/* Menu Text (Tr) */}
|
||||||
{translate('::Next') || 'Next'}
|
<FormItem
|
||||||
</Button>
|
label="Menu Text (Tr)"
|
||||||
</>
|
asterisk={true}
|
||||||
|
invalid={!!(errors.languageTextMenuTr && touched.languageTextMenuTr)}
|
||||||
|
errorMessage={errors.languageTextMenuTr}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="languageTextMenuTr"
|
||||||
|
placeholder="Turkish Menu Text"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ─── Fixed Footer ─────────────────────────────── */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-3">
|
||||||
|
<div className="max-w-sm mx-auto">
|
||||||
|
<Button block variant="solid" type="button" onClick={onNext}>
|
||||||
|
{translate('::Next') || 'Next'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
462
ui/src/views/admin/listForm/WizardStep2.tsx
Normal file
462
ui/src/views/admin/listForm/WizardStep2.tsx
Normal file
|
|
@ -0,0 +1,462 @@
|
||||||
|
import { Button, FormItem, Input, Select } from '@/components/ui'
|
||||||
|
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
|
||||||
|
import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
|
||||||
|
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
|
||||||
|
import { SelectBoxOption } from '@/types/shared'
|
||||||
|
import { dbSourceTypeOptions, selectCommandTypeOptions } from './edit/options'
|
||||||
|
import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
|
||||||
|
import CreatableSelect from 'react-select/creatable'
|
||||||
|
|
||||||
|
// ─── SQL dataType → DbTypeEnum mapper ────────────────────────────────────────
|
||||||
|
|
||||||
|
export function sqlDataTypeToDbType(sqlType: string): DbTypeEnum {
|
||||||
|
const t = sqlType
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s*\(.*\)/, '')
|
||||||
|
.trim()
|
||||||
|
if (['int', 'integer', 'int32'].includes(t)) return DbTypeEnum.Int32
|
||||||
|
if (['bigint', 'int64'].includes(t)) return DbTypeEnum.Int64
|
||||||
|
if (['smallint', 'int16'].includes(t)) return DbTypeEnum.Int16
|
||||||
|
if (['tinyint', 'byte'].includes(t)) return DbTypeEnum.Byte
|
||||||
|
if (['bit', 'boolean', 'bool'].includes(t)) return DbTypeEnum.Boolean
|
||||||
|
if (['float', 'real', 'double', 'double precision'].includes(t)) return DbTypeEnum.Double
|
||||||
|
if (['decimal', 'numeric', 'money', 'smallmoney'].includes(t)) return DbTypeEnum.Decimal
|
||||||
|
if (['uniqueidentifier'].includes(t)) return DbTypeEnum.Guid
|
||||||
|
if (['datetime2', 'smalldatetime', 'datetime'].includes(t)) return DbTypeEnum.DateTime
|
||||||
|
if (['date'].includes(t)) return DbTypeEnum.Date
|
||||||
|
if (['time'].includes(t)) return DbTypeEnum.Time
|
||||||
|
if (['datetimeoffset'].includes(t)) return DbTypeEnum.DateTimeOffset
|
||||||
|
if (['nvarchar', 'varchar', 'char', 'nchar', 'text', 'ntext', 'string'].includes(t))
|
||||||
|
return DbTypeEnum.String
|
||||||
|
if (['xml'].includes(t)) return DbTypeEnum.Xml
|
||||||
|
if (['binary', 'varbinary', 'image'].includes(t)) return DbTypeEnum.Binary
|
||||||
|
return DbTypeEnum.String
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Props ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface WizardStep2Props {
|
||||||
|
values: ListFormWizardDto
|
||||||
|
errors: FormikErrors<ListFormWizardDto>
|
||||||
|
touched: FormikTouched<ListFormWizardDto>
|
||||||
|
// Data Source
|
||||||
|
isLoadingDataSource: boolean
|
||||||
|
dataSourceList: SelectBoxOption[]
|
||||||
|
isDataSourceNew: boolean
|
||||||
|
onDataSourceSelect: (value: string) => void
|
||||||
|
onDataSourceNewChange: (isNew: boolean) => void
|
||||||
|
// DB Objects
|
||||||
|
dbObjects: SqlObjectExplorerDto | null
|
||||||
|
isLoadingDbObjects: boolean
|
||||||
|
// Columns
|
||||||
|
selectCommandColumns: DatabaseColumnDto[]
|
||||||
|
isLoadingColumns: boolean
|
||||||
|
selectedColumns: Set<string>
|
||||||
|
onLoadColumns: (dsCode: string, schema: string, name: string) => void
|
||||||
|
onClearColumns: () => void
|
||||||
|
onToggleColumn: (col: string) => void
|
||||||
|
onToggleAllColumns: (all: boolean) => void
|
||||||
|
// Navigation
|
||||||
|
translate: (key: string) => string
|
||||||
|
onBack: () => void
|
||||||
|
onNext: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── WizardStep2 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const WizardStep2 = ({
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
isLoadingDataSource,
|
||||||
|
dataSourceList,
|
||||||
|
isDataSourceNew,
|
||||||
|
onDataSourceSelect,
|
||||||
|
onDataSourceNewChange,
|
||||||
|
dbObjects,
|
||||||
|
isLoadingDbObjects,
|
||||||
|
selectCommandColumns,
|
||||||
|
isLoadingColumns,
|
||||||
|
selectedColumns,
|
||||||
|
onLoadColumns,
|
||||||
|
onClearColumns,
|
||||||
|
onToggleColumn,
|
||||||
|
onToggleAllColumns,
|
||||||
|
translate,
|
||||||
|
onBack,
|
||||||
|
onNext,
|
||||||
|
}: WizardStep2Props) => {
|
||||||
|
return (
|
||||||
|
<div className="pb-20">
|
||||||
|
{/* ListForm Code + Data Source */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||||
|
<FormItem
|
||||||
|
label="ListForm Code"
|
||||||
|
invalid={!!(errors.listFormCode && touched.listFormCode)}
|
||||||
|
errorMessage={errors.listFormCode}
|
||||||
|
asterisk={true}
|
||||||
|
extra={
|
||||||
|
<span className="text-xs ml-2 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>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Data Source Code"
|
||||||
|
asterisk={true}
|
||||||
|
invalid={!!(errors.dataSourceCode && touched.dataSourceCode)}
|
||||||
|
errorMessage={errors.dataSourceCode}
|
||||||
|
>
|
||||||
|
<Field type="text" autoComplete="off" name="dataSourceCode">
|
||||||
|
{({ field, form }: FieldProps<string>) => (
|
||||||
|
<Select
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
placeholder="Data Source Code"
|
||||||
|
isClearable={true}
|
||||||
|
isLoading={isLoadingDataSource}
|
||||||
|
options={dataSourceList}
|
||||||
|
value={
|
||||||
|
values.dataSourceCode
|
||||||
|
? (dataSourceList?.find((o) => o.value === values.dataSourceCode) ?? {
|
||||||
|
label: values.dataSourceCode,
|
||||||
|
value: values.dataSourceCode,
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onChange={(option) => {
|
||||||
|
const val = option?.value ?? ''
|
||||||
|
form.setFieldValue(field.name, val)
|
||||||
|
onDataSourceSelect(val)
|
||||||
|
onDataSourceNewChange(
|
||||||
|
!!val && !dataSourceList.some((a) => a.value === val),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
{isDataSourceNew && (
|
||||||
|
<FormItem
|
||||||
|
label="Connection String"
|
||||||
|
invalid={!!(errors.dataSourceConnectionString && touched.dataSourceConnectionString)}
|
||||||
|
errorMessage={errors.dataSourceConnectionString}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="dataSourceConnectionString"
|
||||||
|
placeholder="Connection String"
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Title Text (En)"
|
||||||
|
invalid={!!(errors.languageTextTitleEn && touched.languageTextTitleEn)}
|
||||||
|
errorMessage={errors.languageTextTitleEn}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="languageTextTitleEn"
|
||||||
|
placeholder="English Title Text"
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Title Text (Tr)"
|
||||||
|
invalid={!!(errors.languageTextTitleTr && touched.languageTextTitleTr)}
|
||||||
|
errorMessage={errors.languageTextTitleTr}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="languageTextTitleTr"
|
||||||
|
placeholder="Turkish Title Text"
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Description Text (En)"
|
||||||
|
invalid={!!(errors.languageTextDescEn && touched.languageTextDescEn)}
|
||||||
|
errorMessage={errors.languageTextDescEn}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="languageTextDescEn"
|
||||||
|
placeholder="English Description Text"
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Description Text (Tr)"
|
||||||
|
invalid={!!(errors.languageTextDescTr && touched.languageTextDescTr)}
|
||||||
|
errorMessage={errors.languageTextDescTr}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
name="languageTextDescTr"
|
||||||
|
placeholder="Turkish Description Text"
|
||||||
|
component={Input}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Select Command + Key Field Name */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
||||||
|
<FormItem
|
||||||
|
label="Select Command"
|
||||||
|
invalid={!!(errors.selectCommand && touched.selectCommand)}
|
||||||
|
errorMessage={errors.selectCommand}
|
||||||
|
asterisk={true}
|
||||||
|
extra={
|
||||||
|
values.selectCommandType ? (
|
||||||
|
<span className="ml-2 text-xs px-2 py-0.5 rounded bg-indigo-100 text-indigo-600 dark:bg-indigo-900/40 dark:text-indigo-300">
|
||||||
|
{selectCommandTypeOptions.find(
|
||||||
|
(o: any) => o.value === values.selectCommandType,
|
||||||
|
)?.label ?? String(values.selectCommandType)}
|
||||||
|
</span>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Field type="text" autoComplete="off" name="selectCommand">
|
||||||
|
{({ field, form }: FieldProps<string>) => {
|
||||||
|
const grouped = dbObjects
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Tables',
|
||||||
|
options: dbObjects.tables.map((t) => ({
|
||||||
|
label: t.tableName,
|
||||||
|
value: t.tableName,
|
||||||
|
__type: SelectCommandTypeEnum.Table,
|
||||||
|
__schema: t.schemaName,
|
||||||
|
__rawName: t.tableName,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Stored Procedures',
|
||||||
|
options: dbObjects.storedProcedures.map((p) => ({
|
||||||
|
label: p.procedureName,
|
||||||
|
value: p.procedureName,
|
||||||
|
__type: SelectCommandTypeEnum.StoredProcedure,
|
||||||
|
__schema: p.schemaName,
|
||||||
|
__rawName: p.procedureName,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Views',
|
||||||
|
options: dbObjects.views.map((v) => ({
|
||||||
|
label: v.viewName,
|
||||||
|
value: v.viewName,
|
||||||
|
__type: SelectCommandTypeEnum.View,
|
||||||
|
__schema: v.schemaName,
|
||||||
|
__rawName: v.viewName,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Functions',
|
||||||
|
options: dbObjects.functions.map((f) => ({
|
||||||
|
label: f.functionName,
|
||||||
|
value: f.functionName,
|
||||||
|
__type: SelectCommandTypeEnum.TableValuedFunction,
|
||||||
|
__schema: f.schemaName,
|
||||||
|
__rawName: f.functionName,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
componentAs={CreatableSelect}
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
isClearable
|
||||||
|
isLoading={isLoadingDbObjects}
|
||||||
|
options={grouped}
|
||||||
|
placeholder={isLoadingDbObjects ? 'Loading…' : 'Tablo/View/SP seç veya SQL yaz…'}
|
||||||
|
value={field.value ? { label: field.value, value: field.value } : null}
|
||||||
|
onChange={(option: any) => {
|
||||||
|
if (!option) {
|
||||||
|
form.setFieldValue(field.name, '')
|
||||||
|
onClearColumns()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.setFieldValue(field.name, option.value)
|
||||||
|
const type = option.__isNew__
|
||||||
|
? SelectCommandTypeEnum.Query
|
||||||
|
: (option.__type ?? SelectCommandTypeEnum.Query)
|
||||||
|
form.setFieldValue('selectCommandType', type)
|
||||||
|
form.setFieldValue('keyFieldName', '')
|
||||||
|
form.setFieldTouched('keyFieldName', false)
|
||||||
|
if (!option.__isNew__ && option.__schema != null && option.__rawName) {
|
||||||
|
onLoadColumns(values.dataSourceCode, option.__schema, option.__rawName)
|
||||||
|
} else {
|
||||||
|
onClearColumns()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCreateOption={(inputValue: string) => {
|
||||||
|
form.setFieldValue(field.name, inputValue)
|
||||||
|
form.setFieldValue('selectCommandType', SelectCommandTypeEnum.Query)
|
||||||
|
form.setFieldValue('keyFieldName', '')
|
||||||
|
form.setFieldTouched('keyFieldName', false)
|
||||||
|
onClearColumns()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
label="Key Field Name"
|
||||||
|
invalid={!!(errors.keyFieldName && touched.keyFieldName)}
|
||||||
|
errorMessage={errors.keyFieldName}
|
||||||
|
asterisk={true}
|
||||||
|
extra={
|
||||||
|
values.keyFieldName && values.keyFieldDbSourceType != null ? (
|
||||||
|
<span className="ml-2 text-xs px-2 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
|
||||||
|
{dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType)
|
||||||
|
?.label ?? String(values.keyFieldDbSourceType)}
|
||||||
|
</span>
|
||||||
|
) : selectCommandColumns.length === 0 && !isLoadingColumns ? (
|
||||||
|
<span className="text-xs ml-2 text-gray-400">
|
||||||
|
Select Command seçince sütunlar yüklenir
|
||||||
|
</span>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Field type="text" autoComplete="off" name="keyFieldName">
|
||||||
|
{({ field, form }: FieldProps<string>) => (
|
||||||
|
<Select
|
||||||
|
componentAs={CreatableSelect}
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
isClearable
|
||||||
|
isLoading={isLoadingColumns}
|
||||||
|
placeholder={isLoadingColumns ? 'Sütunlar yükleniyor…' : 'Key sütunu seç…'}
|
||||||
|
options={selectCommandColumns.map((c) => ({
|
||||||
|
label: `${c.columnName} (${c.dataType})`,
|
||||||
|
value: c.columnName,
|
||||||
|
__dataType: c.dataType,
|
||||||
|
}))}
|
||||||
|
value={field.value ? { label: field.value, value: field.value } : null}
|
||||||
|
onChange={(option: any) => {
|
||||||
|
if (!option) {
|
||||||
|
form.setFieldValue(field.name, '')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.setFieldValue(field.name, option.value)
|
||||||
|
if (!option.__isNew__ && option.__dataType) {
|
||||||
|
form.setFieldValue(
|
||||||
|
'keyFieldDbSourceType',
|
||||||
|
sqlDataTypeToDbType(option.__dataType),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCreateOption={(inputValue: string) => {
|
||||||
|
form.setFieldValue(field.name, inputValue)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column Selection Panel */}
|
||||||
|
<FormItem
|
||||||
|
label="Sütunlar"
|
||||||
|
extra={
|
||||||
|
selectCommandColumns.length > 0 ? (
|
||||||
|
<div className="flex items-center gap-2 ml-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onToggleAllColumns(true)}
|
||||||
|
className="text-xs px-2 py-0.5 rounded bg-indigo-500 text-white hover:bg-indigo-600"
|
||||||
|
>
|
||||||
|
Tümünü Seç
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onToggleAllColumns(false)}
|
||||||
|
className="text-xs px-2 py-0.5 rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
|
||||||
|
>
|
||||||
|
Tümünü Kaldır
|
||||||
|
</button>
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{selectedColumns.size}/{selectCommandColumns.length} sütun
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 overflow-hidden">
|
||||||
|
{isLoadingColumns ? (
|
||||||
|
<div className="px-4 py-3 text-sm text-gray-400">Sütunlar yükleniyor…</div>
|
||||||
|
) : selectCommandColumns.length === 0 ? (
|
||||||
|
<div className="px-4 py-3 text-sm text-gray-400">
|
||||||
|
Select Command seçilince sütunlar burada görünecek
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-40 overflow-y-auto py-1">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-0 px-1">
|
||||||
|
{selectCommandColumns.map((col) => (
|
||||||
|
<label
|
||||||
|
key={col.columnName}
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 rounded mx-0.5 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedColumns.has(col.columnName)}
|
||||||
|
onChange={() => onToggleColumn(col.columnName)}
|
||||||
|
className="w-3.5 h-3.5 accent-indigo-500 shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="flex flex-col min-w-0">
|
||||||
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-200 truncate">
|
||||||
|
{col.columnName}
|
||||||
|
</span>
|
||||||
|
<span className="text-[12px] text-gray-400 dark:text-gray-500 truncate">
|
||||||
|
{col.dataType}
|
||||||
|
{col.isNullable ? '?' : ''}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
{/* ─── Fixed Footer ─────────────────────────────── */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 px-6 py-3">
|
||||||
|
<div className="max-w-sm mx-auto flex gap-3">
|
||||||
|
<Button block variant="default" type="button" onClick={onBack}>
|
||||||
|
{translate('::Back') || 'Back'}
|
||||||
|
</Button>
|
||||||
|
<Button block variant="solid" type="button" onClick={onNext}>
|
||||||
|
{translate('::Next') || 'Next'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WizardStep2
|
||||||
Loading…
Reference in a new issue