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",
|
||||
"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",
|
||||
"key": "App.Listforms.DataSource",
|
||||
|
|
|
|||
|
|
@ -1,30 +1,20 @@
|
|||
import Container from '@/components/shared/Container'
|
||||
import {
|
||||
Button,
|
||||
FormContainer,
|
||||
FormItem,
|
||||
Input,
|
||||
Notification,
|
||||
Select,
|
||||
Steps,
|
||||
toast,
|
||||
} from '@/components/ui'
|
||||
import { Button, FormContainer, Notification, Steps, toast } from '@/components/ui'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { ListFormWizardDto } from '@/proxy/admin/list-form/models'
|
||||
import { SelectBoxOption } from '@/types/shared'
|
||||
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 { Helmet } from 'react-helmet'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import CreatableSelect from 'react-select/creatable'
|
||||
import * as Yup from 'yup'
|
||||
import { dbSourceTypeOptions, selectCommandTypeOptions } from './edit/options'
|
||||
import { getMenus } from '@/services/menu.service'
|
||||
import { getPermissions } from '@/services/identity.service'
|
||||
import { DbTypeEnum, SelectCommandTypeEnum } from '@/proxy/form/models'
|
||||
import { postListFormWizard } from '@/services/admin/list-form.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 { MenuItem } from '@/proxy/menus/menu'
|
||||
import WizardStep1, {
|
||||
|
|
@ -33,10 +23,10 @@ import WizardStep1, {
|
|||
filterNonLinkNodes,
|
||||
findRootCode,
|
||||
} from './WizardStep1'
|
||||
|
||||
import WizardStep2, { sqlDataTypeToDbType } from './WizardStep2'
|
||||
import { Container } from '@/components/shared'
|
||||
|
||||
// ─── Formik initial values & validation ──────────────────────────────────────
|
||||
|
||||
const initialValues: ListFormWizardDto = {
|
||||
listFormCode: '',
|
||||
menuCode: '',
|
||||
|
|
@ -73,7 +63,7 @@ const step2ValidationSchema = Yup.object().shape({
|
|||
languageTextTitleTr: Yup.string(),
|
||||
languageTextDescEn: Yup.string(),
|
||||
languageTextDescTr: Yup.string(),
|
||||
dataSourceCode: Yup.string(),
|
||||
dataSourceCode: Yup.string().required(),
|
||||
dataSourceConnectionString: Yup.string(),
|
||||
selectCommandType: Yup.string().required(),
|
||||
selectCommand: Yup.string().required(),
|
||||
|
|
@ -93,6 +83,73 @@ const Wizard = () => {
|
|||
const [isLoadingDataSource, setIsLoadingDataSource] = useState(false)
|
||||
const [dataSourceList, setDataSourceList] = useState<SelectBoxOption[]>([])
|
||||
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 () => {
|
||||
setIsLoadingDataSource(true)
|
||||
const response = await getDataSources()
|
||||
|
|
@ -212,6 +269,20 @@ const Wizard = () => {
|
|||
|
||||
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 (
|
||||
<Container>
|
||||
<Helmet
|
||||
|
|
@ -226,294 +297,123 @@ const Wizard = () => {
|
|||
<Steps.Item
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-2 xl:grid-cols-3">
|
||||
<Formik
|
||||
innerRef={formikRef}
|
||||
initialValues={{ ...initialValues }}
|
||||
validationSchema={listFormValidationSchema}
|
||||
onSubmit={async (values, { setSubmitting }) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await postListFormWizard({ ...values })
|
||||
toast.push(
|
||||
<Notification type="success" duration={2000}>
|
||||
{translate('::ListForms.FormBilgileriKaydedildi')}
|
||||
</Notification>,
|
||||
{ placement: 'top-end' },
|
||||
<Formik
|
||||
innerRef={formikRef}
|
||||
initialValues={{ ...initialValues }}
|
||||
validationSchema={listFormValidationSchema}
|
||||
onSubmit={async (values, { setSubmitting }) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await postListFormWizard({ ...values })
|
||||
toast.push(
|
||||
<Notification type="success" duration={2000}>
|
||||
{translate('::ListForms.FormBilgileriKaydedildi')}
|
||||
</Notification>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
setSubmitting(false)
|
||||
setTimeout(() => {
|
||||
navigate(
|
||||
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(
|
||||
':listFormCode',
|
||||
values.listFormCode,
|
||||
),
|
||||
)
|
||||
setSubmitting(false)
|
||||
setTimeout(() => {
|
||||
navigate(
|
||||
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(
|
||||
':listFormCode',
|
||||
values.listFormCode,
|
||||
),
|
||||
)
|
||||
}, 500)
|
||||
} catch (error: any) {
|
||||
toast.push(<Notification title={error.message} type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ touched, errors, isSubmitting, values }) => (
|
||||
<Form>
|
||||
<FormContainer size="sm">
|
||||
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
||||
{currentStep === 0 && (
|
||||
<WizardStep1
|
||||
values={values}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
wizardName={wizardName}
|
||||
onWizardNameChange={handleWizardNameChange}
|
||||
rawMenuItems={rawMenuItems}
|
||||
menuTree={menuTree}
|
||||
isLoadingMenu={isLoadingMenu}
|
||||
onMenuParentChange={handleMenuParentChange}
|
||||
onClearMenuParent={() => formikRef.current?.setFieldValue('menuParentCode', '')}
|
||||
onReloadMenu={getMenuList}
|
||||
permissionGroupList={permissionGroupList}
|
||||
isLoadingPermissionGroup={isLoadingPermissionGroup}
|
||||
onNext={handleNext}
|
||||
translate={translate}
|
||||
/>
|
||||
)}
|
||||
}, 500)
|
||||
} catch (error: any) {
|
||||
toast.push(<Notification title={error.message} type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ touched, errors, isSubmitting, values }) => (
|
||||
<Form>
|
||||
<FormContainer size="sm">
|
||||
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
||||
{currentStep === 0 && (
|
||||
<WizardStep1
|
||||
values={values}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
wizardName={wizardName}
|
||||
onWizardNameChange={handleWizardNameChange}
|
||||
rawMenuItems={rawMenuItems}
|
||||
menuTree={menuTree}
|
||||
isLoadingMenu={isLoadingMenu}
|
||||
onMenuParentChange={handleMenuParentChange}
|
||||
onClearMenuParent={() => formikRef.current?.setFieldValue('menuParentCode', '')}
|
||||
onReloadMenu={getMenuList}
|
||||
permissionGroupList={permissionGroupList}
|
||||
isLoadingPermissionGroup={isLoadingPermissionGroup}
|
||||
onNext={handleNext}
|
||||
translate={translate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ─── Step 2: Data Settings ───────────────────────────── */}
|
||||
{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>
|
||||
{/* ─── Step 2: Data Settings ───────────────────────────── */}
|
||||
{currentStep === 1 && (
|
||||
<WizardStep2
|
||||
values={values}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
isLoadingDataSource={isLoadingDataSource}
|
||||
dataSourceList={dataSourceList}
|
||||
isDataSourceNew={isDataSourceNew}
|
||||
onDataSourceSelect={setCurrentDataSource}
|
||||
onDataSourceNewChange={setIsDataSourceNew}
|
||||
dbObjects={dbObjects}
|
||||
isLoadingDbObjects={isLoadingDbObjects}
|
||||
selectCommandColumns={selectCommandColumns}
|
||||
isLoadingColumns={isLoadingColumns}
|
||||
selectedColumns={selectedColumns}
|
||||
onLoadColumns={loadColumns}
|
||||
onClearColumns={() => {
|
||||
setSelectCommandColumns([])
|
||||
setSelectedColumns(new Set())
|
||||
}}
|
||||
onToggleColumn={toggleColumn}
|
||||
onToggleAllColumns={toggleAllColumns}
|
||||
translate={translate}
|
||||
onBack={handleBack}
|
||||
onNext={handleNext2}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<FormItem
|
||||
label="Title (En)"
|
||||
invalid={errors.languageTextTitleEn && touched.languageTextTitleEn}
|
||||
errorMessage={errors.languageTextTitleEn}
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
name="languageTextTitleEn"
|
||||
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>
|
||||
{/* ─── Step 3: List Form Fields ───────────────────────────── */}
|
||||
{currentStep === 2 && (
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button block variant="default" type="button" onClick={() => setCurrentStep(1)}>
|
||||
{translate('::Back') || 'Back'}
|
||||
</Button>
|
||||
<Button block variant="solid" type="button" onClick={() => setCurrentStep(3)}>
|
||||
{translate('::Next') || 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<FormItem
|
||||
label="Description (En)"
|
||||
invalid={errors.languageTextDescEn && touched.languageTextDescEn}
|
||||
errorMessage={errors.languageTextDescEn}
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
name="languageTextDescEn"
|
||||
placeholder="Description (En)"
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
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>
|
||||
{/* ─── Step 4: Deploy ───────────────────────────── */}
|
||||
{currentStep === 3 && (
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button block variant="default" type="button" onClick={() => setCurrentStep(2)}>
|
||||
{translate('::Back') || 'Back'}
|
||||
</Button>
|
||||
<Button block variant="solid" loading={isSubmitting} type="submit">
|
||||
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
FormItem,
|
||||
Input,
|
||||
Notification,
|
||||
Select,
|
||||
toast,
|
||||
} from '@/components/ui'
|
||||
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'
|
||||
|
|
@ -102,9 +94,20 @@ interface TreeNodeProps {
|
|||
}
|
||||
|
||||
function TreeNode({
|
||||
node, depth, selectedCode, onSelect, expanded, onToggle,
|
||||
editingCode, editingValue, saving,
|
||||
onStartEdit, onEditChange, onSaveEdit, onCancelEdit, onDelete,
|
||||
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)
|
||||
|
|
@ -125,17 +128,24 @@ function TreeNode({
|
|||
>
|
||||
<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) }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (hasChildren) onToggle(node.code)
|
||||
}}
|
||||
>
|
||||
{hasChildren ? (
|
||||
isExpanded
|
||||
? <FaChevronDown className="text-gray-400" />
|
||||
: <FaChevronRight className="text-gray-400" />
|
||||
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'}`} />
|
||||
<NodeIcon
|
||||
className={`text-base shrink-0 ${isSelected ? 'text-indigo-200' : 'text-gray-400'}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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"
|
||||
/>
|
||||
) : (
|
||||
<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}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -165,52 +178,76 @@ function TreeNode({
|
|||
|
||||
{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">
|
||||
<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">
|
||||
<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'}`}>
|
||||
<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'}`}>
|
||||
<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}
|
||||
/>
|
||||
))}
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
|
@ -227,14 +264,26 @@ interface MenuTreeInlineProps {
|
|||
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 [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 })
|
||||
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)
|
||||
|
|
@ -247,7 +296,10 @@ function MenuTreeInline({ value, onChange, nodes, rawItems, isLoading, invalid,
|
|||
try {
|
||||
const original = rawItems.find((i) => i.code === node.code)
|
||||
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()
|
||||
setEditingCode(null)
|
||||
} catch (e: any) {
|
||||
|
|
@ -279,23 +331,37 @@ function MenuTreeInline({ value, onChange, nodes, rawItems, isLoading, invalid,
|
|||
const enrichedNodes = nodes.map(enrichNode)
|
||||
|
||||
const sharedNodeProps = {
|
||||
expanded, onToggle: toggle,
|
||||
editingCode, editingValue, saving,
|
||||
onStartEdit: handleStartEdit, onEditChange: setEditingValue,
|
||||
onSaveEdit: handleSaveEdit, onCancelEdit: () => setEditingCode(null),
|
||||
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">
|
||||
<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-72 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} />
|
||||
<TreeNode
|
||||
key={node.code}
|
||||
node={node}
|
||||
depth={0}
|
||||
selectedCode={value}
|
||||
onSelect={onChange}
|
||||
{...sharedNodeProps}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -327,7 +393,9 @@ function IconPickerField({ value, onChange, invalid }: IconPickerFieldProps) {
|
|||
const displayed = filtered.slice(0, limit)
|
||||
const hasMore = displayed.length < filtered.length
|
||||
|
||||
useEffect(() => { setLimit(ICON_PAGE_SIZE) }, [search])
|
||||
useEffect(() => {
|
||||
setLimit(ICON_PAGE_SIZE)
|
||||
}, [search])
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
|
|
@ -354,7 +422,9 @@ function IconPickerField({ value, onChange, invalid }: IconPickerFieldProps) {
|
|||
) : (
|
||||
<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>
|
||||
|
||||
{open && (
|
||||
|
|
@ -375,7 +445,11 @@ function IconPickerField({ value, onChange, invalid }: IconPickerFieldProps) {
|
|||
key={key}
|
||||
type="button"
|
||||
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
|
||||
${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>
|
||||
{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>
|
||||
<span className="text-xs text-gray-400">
|
||||
{displayed.length} / {filtered.length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLimit((l) => l + ICON_PAGE_SIZE)}
|
||||
|
|
@ -412,12 +488,34 @@ interface MenuAddDialogProps {
|
|||
onSaved: () => void
|
||||
}
|
||||
|
||||
function MenuAddDialog({ isOpen, onClose, initialParentCode, initialOrder, rawItems, onSaved }: MenuAddDialogProps) {
|
||||
const [form, setForm] = useState({ code: '', displayName: '', parentCode: initialParentCode, icon: '', shortName: '', order: initialOrder })
|
||||
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 })
|
||||
if (isOpen)
|
||||
setForm({
|
||||
code: '',
|
||||
displayName: '',
|
||||
parentCode: initialParentCode,
|
||||
icon: '',
|
||||
shortName: '',
|
||||
order: initialOrder,
|
||||
})
|
||||
}, [isOpen, initialParentCode, initialOrder])
|
||||
|
||||
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>
|
||||
<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" />
|
||||
<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" />
|
||||
<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 }))} />
|
||||
<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" />
|
||||
<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" />
|
||||
<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" />
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -510,152 +650,212 @@ export interface WizardStep1Props {
|
|||
}
|
||||
|
||||
const WizardStep1 = ({
|
||||
values, errors, touched,
|
||||
wizardName, onWizardNameChange,
|
||||
rawMenuItems, menuTree, isLoadingMenu,
|
||||
onMenuParentChange, onClearMenuParent, onReloadMenu,
|
||||
permissionGroupList, isLoadingPermissionGroup,
|
||||
onNext, translate,
|
||||
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 (
|
||||
<>
|
||||
<div className="pb-20">
|
||||
{/* 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>}
|
||||
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"
|
||||
placeholder="e.g. Routes, Products, Orders"
|
||||
value={wizardName}
|
||||
autoFocus
|
||||
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}
|
||||
{/* Menu Code / Menu Text / Permission Group — 2-column grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
||||
{/* Col 1 */}
|
||||
<div>
|
||||
{/* 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}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
</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)}
|
||||
{/* Menu Text (En) */}
|
||||
<FormItem
|
||||
label="Menu Text (En)"
|
||||
asterisk={true}
|
||||
invalid={!!(errors.languageTextMenuEn && touched.languageTextMenuEn)}
|
||||
errorMessage={errors.languageTextMenuEn}
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
name="languageTextMenuEn"
|
||||
placeholder="English Menu Text"
|
||||
component={Input}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
</FormItem>
|
||||
|
||||
<Button block className="mt-4" variant="solid" type="button" onClick={onNext}>
|
||||
{translate('::Next') || 'Next'}
|
||||
</Button>
|
||||
</>
|
||||
{/* Menu Text (Tr) */}
|
||||
<FormItem
|
||||
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