Wizard Step2 güncellemesi

This commit is contained in:
Sedat Öztürk 2026-02-27 22:57:13 +03:00
parent 078ba898bd
commit 4c5cfe06f8
4 changed files with 1079 additions and 505 deletions

View file

@ -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",

View file

@ -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,10 +297,13 @@ 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 }}
@ -286,234 +360,60 @@ const Wizard = () => {
{/* ─── 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}
/>
</FormItem>
<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>
<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),
)
}} }}
onToggleColumn={toggleColumn}
onToggleAllColumns={toggleAllColumns}
translate={translate}
onBack={handleBack}
onNext={handleNext2}
/> />
)} )}
</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>
{/* ─── Step 3: List Form Fields ───────────────────────────── */}
{currentStep === 2 && (
<div className="flex gap-2 mt-4"> <div className="flex gap-2 mt-4">
<Button block variant="default" type="button" onClick={handleBack}> <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>
)}
{/* ─── Step 4: Deploy ───────────────────────────── */}
{currentStep === 3 && (
<div className="flex gap-2 mt-4">
<Button block variant="default" type="button" onClick={() => setCurrentStep(2)}>
{translate('::Back') || 'Back'} {translate('::Back') || 'Back'}
</Button> </Button>
<Button block variant="solid" loading={isSubmitting} type="submit"> <Button block variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')} {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button> </Button>
</div> </div>
</>
)} )}
</FormContainer> </FormContainer>
</Form> </Form>
)} )}
</Formik> </Formik>
</div>
</Container> </Container>
) )
} }

View file

@ -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,34 +178,58 @@ 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 &&
node.children.map((child) => (
<TreeNode <TreeNode
key={child.code} key={child.code}
node={child as MenuTreeNode & { id?: string }} node={child as MenuTreeNode & { id?: string }}
@ -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,34 +650,52 @@ 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 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 */} {/* Menu Parent */}
<FormItem <FormItem
label="Menu Parent" label="Menu Parent"
@ -549,7 +707,11 @@ const WizardStep1 = ({
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
setMenuDialogParentCode(values.menuParentCode ? findRootCode(rawMenuItems, values.menuParentCode) : '') setMenuDialogParentCode(
values.menuParentCode
? findRootCode(rawMenuItems, values.menuParentCode)
: '',
)
const selectedItem = rawMenuItems.find((i) => i.code === values.menuParentCode) const selectedItem = rawMenuItems.find((i) => i.code === values.menuParentCode)
setMenuDialogInitialOrder(selectedItem?.order ?? 999) setMenuDialogInitialOrder(selectedItem?.order ?? 999)
setMenuDialogOpen(true) setMenuDialogOpen(true)
@ -561,7 +723,11 @@ const WizardStep1 = ({
{values.menuParentCode && ( {values.menuParentCode && (
<button <button
type="button" type="button"
onClick={(e) => { e.stopPropagation(); e.preventDefault(); onClearMenuParent() }} 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 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 <FaTimes className="text-xs" /> Seçimi Kaldır
@ -593,7 +759,10 @@ const WizardStep1 = ({
rawItems={rawMenuItems} rawItems={rawMenuItems}
onSaved={onReloadMenu} onSaved={onReloadMenu}
/> />
</div>
{/* Col 2 */}
<div>
{/* Menu Code */} {/* Menu Code */}
<FormItem <FormItem
label="Menu Code" label="Menu Code"
@ -602,29 +771,51 @@ const WizardStep1 = ({
asterisk={true} asterisk={true}
extra={<span className="text-xs ml-2 text-gray-400">Auto-derived, editable</span>} 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} /> <Field
type="text"
autoComplete="off"
name="menuCode"
placeholder="e.g. App.Routes, App.Products, App.Orders"
component={Input}
/>
</FormItem> </FormItem>
{/* Menu Text (En) */}
<FormItem <FormItem
label="Menu Text (En)" label="Menu Text (En)"
invalid={errors.languageTextMenuEn && touched.languageTextMenuEn} asterisk={true}
invalid={!!(errors.languageTextMenuEn && touched.languageTextMenuEn)}
errorMessage={errors.languageTextMenuEn} errorMessage={errors.languageTextMenuEn}
> >
<Field type="text" autoComplete="off" name="languageTextMenuEn" placeholder="Menu Text (En)" component={Input} /> <Field
type="text"
autoComplete="off"
name="languageTextMenuEn"
placeholder="English Menu Text"
component={Input}
/>
</FormItem> </FormItem>
{/* Menu Text (Tr) */}
<FormItem <FormItem
label="Menu Text (Tr)" label="Menu Text (Tr)"
invalid={errors.languageTextMenuTr && touched.languageTextMenuTr} asterisk={true}
invalid={!!(errors.languageTextMenuTr && touched.languageTextMenuTr)}
errorMessage={errors.languageTextMenuTr} errorMessage={errors.languageTextMenuTr}
> >
<Field type="text" autoComplete="off" name="languageTextMenuTr" placeholder="Menu Text (Tr)" component={Input} /> <Field
type="text"
autoComplete="off"
name="languageTextMenuTr"
placeholder="Turkish Menu Text"
component={Input}
/>
</FormItem> </FormItem>
{/* Permission Group Name */} {/* Permission Group Name */}
<FormItem <FormItem
label="Permission Group Name" label="Permission Group Name"
invalid={errors.permissionGroupName && touched.permissionGroupName} invalid={!!(errors.permissionGroupName && touched.permissionGroupName)}
errorMessage={errors.permissionGroupName} errorMessage={errors.permissionGroupName}
asterisk={true} asterisk={true}
> >
@ -640,7 +831,9 @@ const WizardStep1 = ({
options={permissionGroupList} options={permissionGroupList}
value={ value={
values.permissionGroupName values.permissionGroupName
? (permissionGroupList?.find((o) => o.value === values.permissionGroupName) ?? { ? (permissionGroupList?.find(
(o) => o.value === values.permissionGroupName,
) ?? {
label: values.permissionGroupName, label: values.permissionGroupName,
value: values.permissionGroupName, value: values.permissionGroupName,
}) })
@ -651,11 +844,18 @@ const WizardStep1 = ({
)} )}
</Field> </Field>
</FormItem> </FormItem>
</div>
</div>
<Button block className="mt-4" variant="solid" type="button" onClick={onNext}> {/* ─── 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'} {translate('::Next') || 'Next'}
</Button> </Button>
</> </div>
</div>
</div>
) )
} }

View 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