Wizard AutoFill Required Fields

This commit is contained in:
Sedat ÖZTÜRK 2026-09-04 09:28:42 +03:00
parent 47d9a20998
commit e237cacc2a
8 changed files with 816 additions and 121 deletions

View file

@ -16248,6 +16248,24 @@
"en": "Required", "en": "Required",
"tr": "Gerekli" "tr": "Gerekli"
}, },
{
"resourceName": "Platform",
"key": "App.ListFormWizard.RequiredFieldsMissing",
"en": "Fill in the required column mappings before continuing.",
"tr": "Devam etmeden önce zorunlu kolon eşleşmelerini doldurun."
},
{
"resourceName": "Platform",
"key": "App.ListFormWizard.AutoFillRequired",
"en": "Auto Fill Required Fields",
"tr": "Zorunlu Alanları Otomatik Doldur"
},
{
"resourceName": "Platform",
"key": "App.ListFormWizard.AutoFillUnmatched",
"en": "Some required fields could not be matched from the select command columns; please select them manually.",
"tr": "Bazı zorunlu alanlar select command kolonlarından eşleştirilemedi; elle seçmelisiniz."
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.ListFormWizard.ListFormFields", "key": "App.ListFormWizard.ListFormFields",

View file

@ -937,7 +937,7 @@ const FileManager = () => {
</div> </div>
<Select <Select
size="xs" size="sm"
options={sortOptions} options={sortOptions}
value={selectedSortOption} value={selectedSortOption}
onChange={(option) => { onChange={(option) => {
@ -976,7 +976,7 @@ const FileManager = () => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isHostContext ? ( {isHostContext ? (
<Select <Select
size="xs" size="sm"
isLoading={tenantsLoading} isLoading={tenantsLoading}
options={tenantOptions} options={tenantOptions}
value={selectedTenantOption} value={selectedTenantOption}

View file

@ -3,8 +3,8 @@ import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { SelectBoxOption } from '@/types/shared' import type { SelectBoxOption } from '@/types/shared'
import { Field, type FieldProps, useFormikContext } from 'formik' import { Field, type FieldProps, useFormikContext } from 'formik'
import { useMemo } from 'react' import { useMemo, useState } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa' import { FaArrowLeft, FaArrowRight, FaMagic } from 'react-icons/fa'
import { gantScaleOptions } from '../edit/options' import { gantScaleOptions } from '../edit/options'
interface WizardStepGanttLayoutProps { interface WizardStepGanttLayoutProps {
@ -14,6 +14,99 @@ interface WizardStepGanttLayoutProps {
onNext: () => void onNext: () => void
} }
type RequiredField =
'keyExpr' | 'parentIdExpr' | 'titleExpr' | 'startExpr' | 'endExpr' | 'progressExpr'
type ColumnKind = 'any' | 'date' | 'number' | 'text'
/**
* Gantt görünümünün çalışması için şart olan eşleşmeler. Liste `views/list/List.tsx`
* içindeki `isLayoutValid` kontrolüyle aynıdır: biri boşsa ekran grid'e düşer.
* `hints`, otomatik doldurmada öncelik sırasına göre denenen kolon adı parçalarıdır.
*/
const REQUIRED_FIELDS: Array<{
name: RequiredField
labelKey: string
kind?: ColumnKind
hints: string[]
}> = [
{
name: 'keyExpr',
labelKey: '::App.Listform.ListformField.KeyFieldName',
hints: ['id', 'key', 'kod', 'code', 'recordid'],
},
{
name: 'parentIdExpr',
labelKey: '::App.ListFormEdit.ParentIdExpr',
hints: ['parentid', 'ustid', 'parentkey', 'parentcode', 'ustkayitid', 'parent', 'ust'],
},
{
name: 'titleExpr',
labelKey: '::App.ListFormEdit.TitleExpr',
kind: 'text',
hints: ['title', 'baslik', 'gorevadi', 'taskname', 'konu', 'subject', 'name', 'adi', 'ad'],
},
{
name: 'startExpr',
labelKey: '::App.ListFormEdit.StartExpr',
kind: 'date',
hints: ['startdate', 'baslangictarihi', 'baslangic', 'begindate', 'start', 'begin'],
},
{
name: 'endExpr',
labelKey: '::App.ListFormEdit.EndExpr',
kind: 'date',
hints: ['enddate', 'bitistarihi', 'bitis', 'finishdate', 'duedate', 'end', 'finish'],
},
{
name: 'progressExpr',
labelKey: '::App.ListFormEdit.ProgressExpr',
kind: 'number',
hints: ['progress', 'ilerleme', 'tamamlanma', 'completion', 'percent', 'yuzde', 'oran'],
},
]
const normalize = (value: string) =>
value
.toLowerCase()
.replace(/ı/g, 'i')
.replace(/ş/g, 's')
.replace(/ğ/g, 'g')
.replace(/ü/g, 'u')
.replace(/ö/g, 'o')
.replace(/ç/g, 'c')
.replace(/[^a-z0-9]/g, '')
const kindOf = (dataType: string): ColumnKind => {
const type = (dataType || '').toLowerCase()
if (/date|time|timestamp/.test(type)) return 'date'
if (/int|decimal|numeric|float|real|money|double|number/.test(type)) return 'number'
if (/char|text|string|clob/.test(type)) return 'text'
return 'any'
}
/** Eşleşme yoksa -1; aksi halde kolonun alana ne kadar uyduğunu gösteren skor. */
const scoreColumn = (column: DatabaseColumnDto, hints: string[], kind?: ColumnKind) => {
const name = normalize(column.columnName)
let best = -1
hints.forEach((hint, index) => {
const needle = normalize(hint)
let score = -1
if (name === needle) score = 100
else if (name.endsWith(needle)) score = 70
else if (name.startsWith(needle)) score = 60
else if (name.includes(needle)) score = 40
if (score >= 0 && score - index > best) best = score - index
})
if (best < 0 || !kind || kind === 'any') return best
const columnKind = kindOf(column.dataType)
if (columnKind === kind) return best + 20
// Tarih/sayı bekleyen alana metin kolonu atamak görünümü çalışmaz hale getirir.
if (columnKind !== 'any' && kind !== 'text') return -1
return best
}
/** /**
* Gantt layout adımı: görev başlığı, başlangıç/bitiş tarihi, ilerleme ve hiyerarşi * Gantt layout adımı: görev başlığı, başlangıç/bitiş tarihi, ilerleme ve hiyerarşi
* (anahtar / üst anahtar) kolonlarının eşleştirilmesi ile ölçek ve genişletme davranışı. * (anahtar / üst anahtar) kolonlarının eşleştirilmesi ile ölçek ve genişletme davranışı.
@ -25,7 +118,9 @@ const WizardStepGanttLayout = ({
onBack, onBack,
onNext, onNext,
}: WizardStepGanttLayoutProps) => { }: WizardStepGanttLayoutProps) => {
const { values } = useFormikContext<ListFormWizardDto>() const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
const [errorFields, setErrorFields] = useState<RequiredField[]>([])
const [autoFillMessage, setAutoFillMessage] = useState('')
const fieldOptions = useMemo<SelectBoxOption[]>( const fieldOptions = useMemo<SelectBoxOption[]>(
() => () =>
selectCommandColumns.map((column) => ({ selectCommandColumns.map((column) => ({
@ -35,14 +130,6 @@ const WizardStepGanttLayout = ({
[selectCommandColumns], [selectCommandColumns],
) )
const option = values.ganttOptionDto const option = values.ganttOptionDto
const selectFields = [
['keyExpr', translate('::App.Listform.ListformField.KeyFieldName')],
['parentIdExpr', translate('::App.ListFormEdit.ParentIdExpr')],
['titleExpr', translate('::App.ListFormEdit.TitleExpr')],
['startExpr', translate('::App.ListFormEdit.StartExpr')],
['endExpr', translate('::App.ListFormEdit.EndExpr')],
['progressExpr', translate('::App.ListFormEdit.ProgressExpr')],
] as const
const permissionFields = [ const permissionFields = [
['editingEnabled', '::App.ListFormEdit.EditingEnabled'], ['editingEnabled', '::App.ListFormEdit.EditingEnabled'],
['allowTaskAdding', '::App.ListFormEdit.AllowTaskAdding'], ['allowTaskAdding', '::App.ListFormEdit.AllowTaskAdding'],
@ -56,27 +143,93 @@ const WizardStepGanttLayout = ({
['allowTaskResourceUpdating', '::App.ListFormEdit.AllowTaskResourceUpdating'], ['allowTaskResourceUpdating', '::App.ListFormEdit.AllowTaskResourceUpdating'],
] as const ] as const
const autoFillRequired = () => {
const used = new Set<string>()
let unmatched = false
REQUIRED_FIELDS.forEach((field) => {
const existing = option[field.name]?.trim()
if (existing) {
used.add(existing)
return
}
// Anahtar alan Settings adımında zaten seçildi; tahmin etmeye gerek yok.
const preferred = field.name === 'keyExpr' ? values.keyFieldName?.trim() : ''
if (
preferred &&
!used.has(preferred) &&
selectCommandColumns.some((column) => column.columnName === preferred)
) {
used.add(preferred)
setFieldValue(`ganttOptionDto.${field.name}`, preferred)
return
}
let bestColumn = ''
let bestScore = -1
selectCommandColumns.forEach((column) => {
if (used.has(column.columnName)) return
const score = scoreColumn(column, field.hints, field.kind)
if (score > bestScore) {
bestScore = score
bestColumn = column.columnName
}
})
if (bestScore < 0) {
unmatched = true
return
}
used.add(bestColumn)
setFieldValue(`ganttOptionDto.${field.name}`, bestColumn)
})
setErrorFields([])
setAutoFillMessage(unmatched ? translate('::App.ListFormWizard.AutoFillUnmatched') : '')
}
const continueToNext = () => {
const missing = REQUIRED_FIELDS.filter((field) => !option[field.name]?.trim()).map(
(field) => field.name,
)
setErrorFields(missing)
if (missing.length === 0) onNext()
}
return ( return (
<div className="pb-20"> <div className="pb-20">
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Card header={translate('::App.ListFormSchedulerOptions.BasicSettings')}> <Card header={translate('::App.ListFormSchedulerOptions.BasicSettings')}>
<div className="grid grid-cols-1 gap-x-5 md:grid-cols-2"> <div className="grid grid-cols-1 gap-x-5 md:grid-cols-2">
{selectFields.map(([name, label]) => ( {REQUIRED_FIELDS.map((field) => {
<FormItem key={name} label={label}> const invalid = errorFields.includes(field.name)
<Field name={`ganttOptionDto.${name}`}>
{({ field, form }: FieldProps) => ( return (
<FormItem
key={field.name}
label={`${translate(field.labelKey)} *`}
invalid={invalid}
errorMessage={invalid ? translate('::App.ListFormWizard.Required') : undefined}
>
<Field name={`ganttOptionDto.${field.name}`}>
{({ form, field: formikField }: FieldProps) => (
<Select <Select
field={field} field={formikField}
form={form} form={form}
isClearable
options={fieldOptions} options={fieldOptions}
value={fieldOptions.find((fieldOption) => fieldOption.value === option[name])} isClearable
onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? '')} value={fieldOptions.find(
(fieldOption) => fieldOption.value === option[field.name],
)}
onChange={(selected) => {
form.setFieldValue(formikField.name, selected?.value ?? '')
setErrorFields((current) => current.filter((name) => name !== field.name))
}}
/> />
)} )}
</Field> </Field>
</FormItem> </FormItem>
))} )
})}
<FormItem label={translate('::App.ListFormEdit.HasItemsExpr')}> <FormItem label={translate('::App.ListFormEdit.HasItemsExpr')}>
<Field name="ganttOptionDto.hasItemsExpr" component={Input} /> <Field name="ganttOptionDto.hasItemsExpr" component={Input} />
</FormItem> </FormItem>
@ -121,17 +274,41 @@ const WizardStepGanttLayout = ({
</Card> </Card>
</div> </div>
{autoFillMessage && <p className="mt-4 text-sm text-amber-600">{autoFillMessage}</p>}
{errorFields.length > 0 && (
<p className="mt-2 text-sm text-red-500">
{translate('::App.ListFormWizard.RequiredFieldsMissing')}
</p>
)}
<div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900"> <div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900">
<div className="flex w-full items-center justify-between gap-3"> <div className="flex w-full items-center justify-between gap-3">
<Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}> <Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::App.Platform.Back')} {translate('::App.Platform.Back')}
</Button> </Button>
<Button type="button" size="sm" variant="solid" icon={<FaArrowRight />} onClick={onNext}> <div className="flex items-center gap-3">
<Button
type="button"
size="sm"
icon={<FaMagic />}
disabled={selectCommandColumns.length === 0}
onClick={autoFillRequired}
>
{translate('::App.ListFormWizard.AutoFillRequired')}
</Button>
<Button
type="button"
size="sm"
variant="solid"
icon={<FaArrowRight />}
onClick={continueToNext}
>
{translate('::App.Platform.Next')} {translate('::App.Platform.Next')}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
</div>
) )
} }

View file

@ -15,7 +15,6 @@ import { MenuItem } from '@/proxy/menus/menu'
import { MenuService } from '@/services/menu.service' import { MenuService } from '@/services/menu.service'
import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik' import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import CreatableSelect from 'react-select/creatable'
import { import {
FaArrowDown, FaArrowDown,
FaArrowLeft, FaArrowLeft,

View file

@ -3,8 +3,8 @@ import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { SelectBoxOption } from '@/types/shared' import type { SelectBoxOption } from '@/types/shared'
import { Field, type FieldProps, useFormikContext } from 'formik' import { Field, type FieldProps, useFormikContext } from 'formik'
import { useMemo } from 'react' import { useMemo, useState } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa' import { FaArrowLeft, FaArrowRight, FaMagic } from 'react-icons/fa'
import { firstDayOfWeekOptions, schedulerViewOptions } from '../edit/options' import { firstDayOfWeekOptions, schedulerViewOptions } from '../edit/options'
interface WizardStepSchedulerLayoutProps { interface WizardStepSchedulerLayoutProps {
@ -14,6 +14,88 @@ interface WizardStepSchedulerLayoutProps {
onNext: () => void onNext: () => void
} }
type RequiredField = 'textExpr' | 'startDateExpr' | 'endDateExpr'
type ColumnKind = 'any' | 'date' | 'number' | 'text'
/**
* Takvim görünümünün çalışması için şart olan eşleşmeler. Liste `views/list/List.tsx`
* içindeki `isLayoutValid` kontrolüyle aynıdır: biri boşsa ekran grid'e düşer.
* `hints`, otomatik doldurmada öncelik sırasına göre denenen kolon adı parçalarıdır.
*/
const REQUIRED_FIELDS: Array<{
name: RequiredField
labelKey: string
kind?: ColumnKind
hints: string[]
}> = [
{
name: 'textExpr',
labelKey: '::App.ListFormSchedulerOptions.TextExpr',
kind: 'text',
hints: ['text', 'title', 'baslik', 'konu', 'subject', 'name', 'adi', 'ad'],
},
{
name: 'startDateExpr',
labelKey: '::App.ListFormSchedulerOptions.StartDateExpr',
kind: 'date',
hints: ['startdate', 'baslangictarihi', 'baslangic', 'begindate', 'starttime', 'start'],
},
{
name: 'endDateExpr',
labelKey: '::App.ListFormSchedulerOptions.EndDateExpr',
kind: 'date',
hints: ['enddate', 'bitistarihi', 'bitis', 'finishdate', 'endtime', 'end'],
},
]
/** Tekrar eden randevu alanları; zorunlu değildir. */
const RECURRENCE_FIELDS = [
['recurrenceRuleExpr', '::App.ListFormSchedulerOptions.RecurrenceRuleExpr'],
['recurrenceExceptionExpr', '::App.ListFormSchedulerOptions.RecurrenceExceptionExpr'],
] as const
const normalize = (value: string) =>
value
.toLowerCase()
.replace(/ı/g, 'i')
.replace(/ş/g, 's')
.replace(/ğ/g, 'g')
.replace(/ü/g, 'u')
.replace(/ö/g, 'o')
.replace(/ç/g, 'c')
.replace(/[^a-z0-9]/g, '')
const kindOf = (dataType: string): ColumnKind => {
const type = (dataType || '').toLowerCase()
if (/date|time|timestamp/.test(type)) return 'date'
if (/int|decimal|numeric|float|real|money|double|number/.test(type)) return 'number'
if (/char|text|string|clob/.test(type)) return 'text'
return 'any'
}
/** Eşleşme yoksa -1; aksi halde kolonun alana ne kadar uyduğunu gösteren skor. */
const scoreColumn = (column: DatabaseColumnDto, hints: string[], kind?: ColumnKind) => {
const name = normalize(column.columnName)
let best = -1
hints.forEach((hint, index) => {
const needle = normalize(hint)
let score = -1
if (name === needle) score = 100
else if (name.endsWith(needle)) score = 70
else if (name.startsWith(needle)) score = 60
else if (name.includes(needle)) score = 40
if (score >= 0 && score - index > best) best = score - index
})
if (best < 0 || !kind || kind === 'any') return best
const columnKind = kindOf(column.dataType)
if (columnKind === kind) return best + 20
// Tarih bekleyen alana metin kolonu atamak takvimi çalışmaz hale getirir.
if (columnKind !== 'any' && kind !== 'text') return -1
return best
}
/** /**
* Takvim layout adımı: randevu başlığı, başlangıç/bitiş ve tüm gün gibi Scheduler * Takvim layout adımı: randevu başlığı, başlangıç/bitiş ve tüm gün gibi Scheduler
* görünümünün kolon eşleşmeleri. Yalnızca Scheduler layout'u seçiliyken gösterilir. * görünümünün kolon eşleşmeleri. Yalnızca Scheduler layout'u seçiliyken gösterilir.
@ -24,7 +106,9 @@ const WizardStepSchedulerLayout = ({
onBack, onBack,
onNext, onNext,
}: WizardStepSchedulerLayoutProps) => { }: WizardStepSchedulerLayoutProps) => {
const { values } = useFormikContext<ListFormWizardDto>() const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
const [errorFields, setErrorFields] = useState<RequiredField[]>([])
const [autoFillMessage, setAutoFillMessage] = useState('')
const fieldOptions = useMemo<SelectBoxOption[]>( const fieldOptions = useMemo<SelectBoxOption[]>(
() => () =>
selectCommandColumns.map((column) => ({ selectCommandColumns.map((column) => ({
@ -34,14 +118,6 @@ const WizardStepSchedulerLayout = ({
[selectCommandColumns], [selectCommandColumns],
) )
const option = values.schedulerOptionDto const option = values.schedulerOptionDto
const fieldMappings = [
['textExpr', '::App.ListFormSchedulerOptions.TextExpr'],
['startDateExpr', '::App.ListFormSchedulerOptions.StartDateExpr'],
['endDateExpr', '::App.ListFormSchedulerOptions.EndDateExpr'],
['allDayExpr', '::App.ListFormSchedulerOptions.AllDayExpr'],
['recurrenceRuleExpr', '::App.ListFormSchedulerOptions.RecurrenceRuleExpr'],
['recurrenceExceptionExpr', '::App.ListFormSchedulerOptions.RecurrenceExceptionExpr'],
] as const
const interactionFields = [ const interactionFields = [
['showAllDayPanel', '::App.ListFormSchedulerOptions.ShowAllDayPanel'], ['showAllDayPanel', '::App.ListFormSchedulerOptions.ShowAllDayPanel'],
['crossScrollingEnabled', '::App.ListFormSchedulerOptions.CrossScrollingEnabled'], ['crossScrollingEnabled', '::App.ListFormSchedulerOptions.CrossScrollingEnabled'],
@ -52,26 +128,97 @@ const WizardStepSchedulerLayout = ({
['allowDeleting', '::App.ListFormSchedulerOptions.AllowDeleting'], ['allowDeleting', '::App.ListFormSchedulerOptions.AllowDeleting'],
] as const ] as const
const autoFillRequired = () => {
const used = new Set<string>()
let unmatched = false
REQUIRED_FIELDS.forEach((field) => {
const existing = option[field.name]?.trim()
if (existing) {
used.add(existing)
return
}
let bestColumn = ''
let bestScore = -1
selectCommandColumns.forEach((column) => {
if (used.has(column.columnName)) return
const score = scoreColumn(column, field.hints, field.kind)
if (score > bestScore) {
bestScore = score
bestColumn = column.columnName
}
})
if (bestScore < 0) {
unmatched = true
return
}
used.add(bestColumn)
setFieldValue(`schedulerOptionDto.${field.name}`, bestColumn)
})
setErrorFields([])
setAutoFillMessage(unmatched ? translate('::App.ListFormWizard.AutoFillUnmatched') : '')
}
const continueToNext = () => {
const missing = REQUIRED_FIELDS.filter((field) => !option[field.name]?.trim()).map(
(field) => field.name,
)
setErrorFields(missing)
if (missing.length === 0) onNext()
}
return ( return (
<div className="pb-20"> <div className="pb-20">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<Card header={translate('::App.ListFormSchedulerOptions.BasicSettings')}> <Card header={translate('::App.ListFormSchedulerOptions.BasicSettings')}>
{fieldMappings.slice(0, 4).map(([name, translationKey]) => ( {REQUIRED_FIELDS.map((field) => {
<FormItem key={name} label={translate(translationKey)}> const invalid = errorFields.includes(field.name)
<Field name={`schedulerOptionDto.${name}`}>
return (
<FormItem
key={field.name}
label={`${translate(field.labelKey)} *`}
invalid={invalid}
errorMessage={invalid ? translate('::App.ListFormWizard.Required') : undefined}
>
<Field name={`schedulerOptionDto.${field.name}`}>
{({ form, field: formikField }: FieldProps) => (
<Select
field={formikField}
form={form}
options={fieldOptions}
isClearable={true}
value={fieldOptions.find(
(fieldOption) => fieldOption.value === option[field.name],
)}
onChange={(selected) => {
form.setFieldValue(formikField.name, selected?.value ?? '')
setErrorFields((current) => current.filter((name) => name !== field.name))
}}
/>
)}
</Field>
</FormItem>
)
})}
<FormItem label={translate('::App.ListFormSchedulerOptions.AllDayExpr')}>
<Field name="schedulerOptionDto.allDayExpr">
{({ field, form }: FieldProps) => ( {({ field, form }: FieldProps) => (
<Select <Select
field={field} field={field}
form={form} form={form}
isClearable isClearable={true}
options={fieldOptions} options={fieldOptions}
value={fieldOptions.find((fieldOption) => fieldOption.value === option[name])} value={fieldOptions.find(
(fieldOption) => fieldOption.value === option.allDayExpr,
)}
onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? '')} onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? '')}
/> />
)} )}
</Field> </Field>
</FormItem> </FormItem>
))}
</Card> </Card>
<Card header={translate('::App.ListFormSchedulerOptions.ViewSettings')}> <Card header={translate('::App.ListFormSchedulerOptions.ViewSettings')}>
@ -81,6 +228,7 @@ const WizardStepSchedulerLayout = ({
<Select <Select
field={field} field={field}
form={form} form={form}
isClearable={true}
options={schedulerViewOptions} options={schedulerViewOptions}
value={schedulerViewOptions.find( value={schedulerViewOptions.find(
(viewOption) => viewOption.value === option.defaultView, (viewOption) => viewOption.value === option.defaultView,
@ -96,6 +244,7 @@ const WizardStepSchedulerLayout = ({
<Select <Select
field={field} field={field}
form={form} form={form}
isClearable={true}
options={firstDayOfWeekOptions} options={firstDayOfWeekOptions}
value={firstDayOfWeekOptions.find( value={firstDayOfWeekOptions.find(
(dayOption) => dayOption.value === option.firstDayOfWeek, (dayOption) => dayOption.value === option.firstDayOfWeek,
@ -128,14 +277,14 @@ const WizardStepSchedulerLayout = ({
</Card> </Card>
<Card header={translate('::App.ListFormSchedulerOptions.RecurrenceSettings')}> <Card header={translate('::App.ListFormSchedulerOptions.RecurrenceSettings')}>
{fieldMappings.slice(4).map(([name, translationKey]) => ( {RECURRENCE_FIELDS.map(([name, translationKey]) => (
<FormItem key={name} label={translate(translationKey)}> <FormItem key={name} label={translate(translationKey)}>
<Field name={`schedulerOptionDto.${name}`}> <Field name={`schedulerOptionDto.${name}`}>
{({ field, form }: FieldProps) => ( {({ field, form }: FieldProps) => (
<Select <Select
field={field} field={field}
form={form} form={form}
isClearable isClearable={true}
options={fieldOptions} options={fieldOptions}
value={fieldOptions.find((fieldOption) => fieldOption.value === option[name])} value={fieldOptions.find((fieldOption) => fieldOption.value === option[name])}
onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? '')} onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? '')}
@ -155,17 +304,41 @@ const WizardStepSchedulerLayout = ({
</Card> </Card>
</div> </div>
{autoFillMessage && <p className="mt-4 text-sm text-amber-600">{autoFillMessage}</p>}
{errorFields.length > 0 && (
<p className="mt-2 text-sm text-red-500">
{translate('::App.ListFormWizard.RequiredFieldsMissing')}
</p>
)}
<div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900"> <div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900">
<div className="flex w-full items-center justify-between gap-3"> <div className="flex w-full items-center justify-between gap-3">
<Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}> <Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::App.Platform.Back')} {translate('::App.Platform.Back')}
</Button> </Button>
<Button type="button" size="sm" variant="solid" icon={<FaArrowRight />} onClick={onNext}> <div className="flex items-center gap-3">
<Button
type="button"
size="sm"
icon={<FaMagic />}
disabled={selectCommandColumns.length === 0}
onClick={autoFillRequired}
>
{translate('::App.ListFormWizard.AutoFillRequired')}
</Button>
<Button
type="button"
size="sm"
variant="solid"
icon={<FaArrowRight />}
onClick={continueToNext}
>
{translate('::App.Platform.Next')} {translate('::App.Platform.Next')}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
</div>
) )
} }

View file

@ -5,7 +5,7 @@ import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { SelectBoxOption } from '@/types/shared' import type { SelectBoxOption } from '@/types/shared'
import { Field, type FieldProps, useFormikContext } from 'formik' import { Field, type FieldProps, useFormikContext } from 'formik'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa' import { FaArrowLeft, FaArrowRight, FaMagic } from 'react-icons/fa'
interface WizardStepTodoLayoutProps { interface WizardStepTodoLayoutProps {
selectedColumns: Set<string> selectedColumns: Set<string>
@ -15,6 +15,27 @@ interface WizardStepTodoLayoutProps {
onNext: () => void onNext: () => void
} }
type RequiredField = 'titleExpr' | 'statusExpr'
type ColumnKind = 'any' | 'date' | 'number' | 'text'
/**
* Todo görünümünün çalışması için şart olan eşleşmeler. Liste `views/list/List.tsx`
* içindeki `isLayoutValid` kontrolüyle aynıdır: biri boşsa ekran grid'e düşer.
* `hints`, otomatik doldurmada öncelik sırasına göre denenen kolon adı parçalarıdır.
*/
const REQUIRED_FIELDS: Array<{ name: RequiredField; kind?: ColumnKind; hints: string[] }> = [
{
name: 'titleExpr',
kind: 'text',
hints: ['title', 'baslik', 'gorevadi', 'taskname', 'konu', 'subject', 'name', 'adi', 'ad'],
},
{
name: 'statusExpr',
hints: ['status', 'durum', 'statu', 'state', 'stage', 'asama', 'kolon', 'column'],
},
]
const mappings: Array<{ const mappings: Array<{
name: keyof TodoOptionDto name: keyof TodoOptionDto
translationKey: string translationKey: string
@ -59,6 +80,42 @@ const mappings: Array<{
{ name: 'subTaskExpr', translationKey: '::App.ListFormFieldEdit.TodoSubTaskExpr' }, { name: 'subTaskExpr', translationKey: '::App.ListFormFieldEdit.TodoSubTaskExpr' },
] ]
const normalize = (value: string) =>
value
.toLowerCase()
.replace(/ı/g, 'i')
.replace(/ş/g, 's')
.replace(/ğ/g, 'g')
.replace(/ü/g, 'u')
.replace(/ö/g, 'o')
.replace(/ç/g, 'c')
.replace(/[^a-z0-9]/g, '')
const kindOf = (dataType: string): ColumnKind => {
const type = (dataType || '').toLowerCase()
if (/date|time|timestamp/.test(type)) return 'date'
if (/int|decimal|numeric|float|real|money|double|number/.test(type)) return 'number'
if (/char|text|string|clob/.test(type)) return 'text'
return 'any'
}
/** Eşleşme yoksa -1; aksi halde kolonun alana ne kadar uyduğunu gösteren skor. */
const scoreColumn = (column: DatabaseColumnDto, hints: string[], kind?: ColumnKind) => {
const name = normalize(column.columnName)
let best = -1
hints.forEach((hint, index) => {
const needle = normalize(hint)
let score = -1
if (name === needle) score = 100
else if (name.endsWith(needle)) score = 70
else if (name.startsWith(needle)) score = 60
else if (name.includes(needle)) score = 40
if (score >= 0 && score - index > best) best = score - index
})
if (best < 0 || !kind || kind === 'any') return best
return kindOf(column.dataType) === kind ? best + 20 : best
}
/** /**
* Todo / Kanban layout adımı: kart başlığı, durum kolonu, sıra alanı gibi Kanban görünümünün * Todo / Kanban layout adımı: kart başlığı, durum kolonu, sıra alanı gibi Kanban görünümünün
* ihtiyaç duyduğu kolon eşleşmeleri. Yalnızca Todo layout'u seçiliyken gösterilir. * ihtiyaç duyduğu kolon eşleşmeleri. Yalnızca Todo layout'u seçiliyken gösterilir.
@ -72,19 +129,54 @@ const WizardStepTodoLayout = ({
}: WizardStepTodoLayoutProps) => { }: WizardStepTodoLayoutProps) => {
const { values, setFieldValue } = useFormikContext<ListFormWizardDto>() const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
const [validationError, setValidationError] = useState('') const [validationError, setValidationError] = useState('')
const fieldOptions = useMemo<SelectBoxOption[]>( const [autoFillMessage, setAutoFillMessage] = useState('')
() => // Todo kartları yalnızca ekrana alınan kolonları gösterebilir; eşleşmeler de onlardan seçilir.
selectCommandColumns const availableColumns = useMemo(
.filter((column) => selectedColumns.has(column.columnName)) () => selectCommandColumns.filter((column) => selectedColumns.has(column.columnName)),
.map((column) => ({ value: column.columnName, label: column.columnName })),
[selectCommandColumns, selectedColumns], [selectCommandColumns, selectedColumns],
) )
const fieldOptions = useMemo<SelectBoxOption[]>(
() =>
availableColumns.map((column) => ({ value: column.columnName, label: column.columnName })),
[availableColumns],
)
const autoFillRequired = () => {
const used = new Set<string>()
let unmatched = false
REQUIRED_FIELDS.forEach((field) => {
const existing = values.todoOptionDto[field.name]?.trim()
if (existing) {
used.add(existing)
return
}
let bestColumn = ''
let bestScore = -1
availableColumns.forEach((column) => {
if (used.has(column.columnName)) return
const score = scoreColumn(column, field.hints, field.kind)
if (score > bestScore) {
bestScore = score
bestColumn = column.columnName
}
})
if (bestScore < 0) {
unmatched = true
return
}
used.add(bestColumn)
setFieldValue(`todoOptionDto.${field.name}`, bestColumn)
})
setValidationError('')
setAutoFillMessage(unmatched ? translate('::App.ListFormWizard.AutoFillUnmatched') : '')
}
const continueToDeploy = () => { const continueToDeploy = () => {
if ( const missing = REQUIRED_FIELDS.some((field) => !values.todoOptionDto[field.name]?.trim())
values.todo && if (values.todo && missing) {
(!values.todoOptionDto.titleExpr?.trim() || !values.todoOptionDto.statusExpr?.trim())
) {
setValidationError(translate('::App.ListFormFieldEdit.TodoRequiredFields')) setValidationError(translate('::App.ListFormFieldEdit.TodoRequiredFields'))
return return
} }
@ -147,6 +239,7 @@ const WizardStepTodoLayout = ({
</FormItem> </FormItem>
</div> </div>
{autoFillMessage && <p className="mt-4 text-sm text-amber-600">{autoFillMessage}</p>}
{validationError && <p className="mt-4 text-sm text-red-500">{validationError}</p>} {validationError && <p className="mt-4 text-sm text-red-500">{validationError}</p>}
<div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900"> <div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900">
@ -154,6 +247,16 @@ const WizardStepTodoLayout = ({
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}> <Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::App.Platform.Back')} {translate('::App.Platform.Back')}
</Button> </Button>
<div className="flex items-center gap-3">
<Button
size="sm"
type="button"
icon={<FaMagic />}
disabled={availableColumns.length === 0}
onClick={autoFillRequired}
>
{translate('::App.ListFormWizard.AutoFillRequired')}
</Button>
<Button <Button
size="sm" size="sm"
variant="solid" variant="solid"
@ -166,6 +269,7 @@ const WizardStepTodoLayout = ({
</div> </div>
</div> </div>
</div> </div>
</div>
) )
} }

View file

@ -3,8 +3,8 @@ import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { SelectBoxOption } from '@/types/shared' import type { SelectBoxOption } from '@/types/shared'
import { Field, type FieldProps, useFormikContext } from 'formik' import { Field, type FieldProps, useFormikContext } from 'formik'
import { useMemo } from 'react' import { useMemo, useState } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa' import { FaArrowLeft, FaArrowRight, FaMagic } from 'react-icons/fa'
interface WizardStepTreeLayoutProps { interface WizardStepTreeLayoutProps {
selectCommandColumns: DatabaseColumnDto[] selectCommandColumns: DatabaseColumnDto[]
@ -13,6 +13,53 @@ interface WizardStepTreeLayoutProps {
onNext: () => void onNext: () => void
} }
type RequiredField = 'keyExpr' | 'parentIdExpr'
/**
* Ağaç görünümünün çalışması için şart olan eşleşmeler. Liste `views/list/List.tsx`
* içindeki `isLayoutValid` kontrolüyle aynıdır: biri boşsa ekran grid'e düşer.
* `hints`, otomatik doldurmada öncelik sırasına göre denenen kolon adı parçalarıdır.
*/
const REQUIRED_FIELDS: Array<{ name: RequiredField; labelKey: string; hints: string[] }> = [
{
name: 'keyExpr',
labelKey: '::App.Listform.ListformField.KeyFieldName',
hints: ['id', 'key', 'kod', 'code', 'recordid'],
},
{
name: 'parentIdExpr',
labelKey: '::App.ListFormEdit.ParentIdExpr',
hints: ['parentid', 'ustid', 'parentkey', 'parentcode', 'ustkayitid', 'parent', 'ust'],
},
]
const normalize = (value: string) =>
value
.toLowerCase()
.replace(/ı/g, 'i')
.replace(/ş/g, 's')
.replace(/ğ/g, 'g')
.replace(/ü/g, 'u')
.replace(/ö/g, 'o')
.replace(/ç/g, 'c')
.replace(/[^a-z0-9]/g, '')
/** Eşleşme yoksa -1; aksi halde kolonun ipuçlarına ne kadar uyduğunu gösteren skor. */
const scoreColumn = (columnName: string, hints: string[]) => {
const name = normalize(columnName)
let best = -1
hints.forEach((hint, index) => {
const needle = normalize(hint)
let score = -1
if (name === needle) score = 100
else if (name.endsWith(needle)) score = 70
else if (name.startsWith(needle)) score = 60
else if (name.includes(needle)) score = 40
if (score >= 0 && score - index > best) best = score - index
})
return best
}
/** /**
* Ağaç layout adımı: kayıtları hiyerarşik gösterebilmek için anahtar ve üst anahtar * Ağaç layout adımı: kayıtları hiyerarşik gösterebilmek için anahtar ve üst anahtar
* kolonlarının eşleştirilmesi. Yalnızca Tree layout'u seçiliyken gösterilir. * kolonlarının eşleştirilmesi. Yalnızca Tree layout'u seçiliyken gösterilir.
@ -23,7 +70,9 @@ const WizardStepTreeLayout = ({
onBack, onBack,
onNext, onNext,
}: WizardStepTreeLayoutProps) => { }: WizardStepTreeLayoutProps) => {
const { values } = useFormikContext<ListFormWizardDto>() const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
const [errorFields, setErrorFields] = useState<RequiredField[]>([])
const [autoFillMessage, setAutoFillMessage] = useState('')
const fieldOptions = useMemo<SelectBoxOption[]>( const fieldOptions = useMemo<SelectBoxOption[]>(
() => () =>
selectCommandColumns.map((column) => ({ selectCommandColumns.map((column) => ({
@ -33,38 +82,92 @@ const WizardStepTreeLayout = ({
[selectCommandColumns], [selectCommandColumns],
) )
const selectFields = [ const autoFillRequired = () => {
{ const option = values.treeOptionDto
name: 'treeOptionDto.keyExpr', const used = new Set<string>()
value: values.treeOptionDto.keyExpr, let unmatched = false
label: translate('::App.Listform.ListformField.KeyFieldName'),
}, REQUIRED_FIELDS.forEach((field) => {
{ const existing = option[field.name]?.trim()
name: 'treeOptionDto.parentIdExpr', if (existing) {
value: values.treeOptionDto.parentIdExpr, used.add(existing)
label: translate('::App.ListFormEdit.ParentIdExpr'), return
}, }
] // Anahtar alan Settings adımında zaten seçildi; tahmin etmeye gerek yok.
const preferred = field.name === 'keyExpr' ? values.keyFieldName?.trim() : ''
if (
preferred &&
!used.has(preferred) &&
selectCommandColumns.some((column) => column.columnName === preferred)
) {
used.add(preferred)
setFieldValue(`treeOptionDto.${field.name}`, preferred)
return
}
let bestColumn = ''
let bestScore = -1
selectCommandColumns.forEach((column) => {
if (used.has(column.columnName)) return
const score = scoreColumn(column.columnName, field.hints)
if (score > bestScore) {
bestScore = score
bestColumn = column.columnName
}
})
if (bestScore < 0) {
unmatched = true
return
}
used.add(bestColumn)
setFieldValue(`treeOptionDto.${field.name}`, bestColumn)
})
setErrorFields([])
setAutoFillMessage(unmatched ? translate('::App.ListFormWizard.AutoFillUnmatched') : '')
}
const continueToNext = () => {
const missing = REQUIRED_FIELDS.filter(
(field) => !values.treeOptionDto[field.name]?.trim(),
).map((field) => field.name)
setErrorFields(missing)
if (missing.length === 0) onNext()
}
return ( return (
<div className="pb-20"> <div className="pb-20">
<div className="grid grid-cols-1 gap-x-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-x-6 md:grid-cols-2">
{selectFields.map((item) => ( {REQUIRED_FIELDS.map((item) => {
<FormItem key={item.name} label={item.label}> const invalid = errorFields.includes(item.name)
<Field name={item.name}>
return (
<FormItem
key={item.name}
label={`${translate(item.labelKey)} *`}
invalid={invalid}
errorMessage={invalid ? translate('::App.ListFormWizard.Required') : undefined}
>
<Field name={`treeOptionDto.${item.name}`}>
{({ field, form }: FieldProps) => ( {({ field, form }: FieldProps) => (
<Select <Select
field={field} field={field}
form={form} form={form}
isClearable isClearable={true}
options={fieldOptions} options={fieldOptions}
value={fieldOptions.find((option) => option.value === item.value)} value={fieldOptions.find(
onChange={(option) => form.setFieldValue(field.name, option?.value ?? '')} (option) => option.value === values.treeOptionDto[item.name],
)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value ?? '')
setErrorFields((current) => current.filter((name) => name !== item.name))
}}
/> />
)} )}
</Field> </Field>
</FormItem> </FormItem>
))} )
})}
<FormItem label={translate('::App.ListFormEdit.HasItemsExpr')}> <FormItem label={translate('::App.ListFormEdit.HasItemsExpr')}>
<Field name="treeOptionDto.hasItemsExpr" component={Input} /> <Field name="treeOptionDto.hasItemsExpr" component={Input} />
@ -80,17 +183,41 @@ const WizardStepTreeLayout = ({
</FormItem> </FormItem>
</div> </div>
{autoFillMessage && <p className="mt-4 text-sm text-amber-600">{autoFillMessage}</p>}
{errorFields.length > 0 && (
<p className="mt-2 text-sm text-red-500">
{translate('::App.ListFormWizard.RequiredFieldsMissing')}
</p>
)}
<div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900"> <div className="fixed bottom-0 left-0 right-0 z-10 flex h-16 items-center border-t border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-900">
<div className="flex w-full items-center justify-between gap-3"> <div className="flex w-full items-center justify-between gap-3">
<Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}> <Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::App.Platform.Back')} {translate('::App.Platform.Back')}
</Button> </Button>
<Button type="button" size="sm" variant="solid" icon={<FaArrowRight />} onClick={onNext}> <div className="flex items-center gap-3">
<Button
type="button"
size="sm"
icon={<FaMagic />}
disabled={selectCommandColumns.length === 0}
onClick={autoFillRequired}
>
{translate('::App.ListFormWizard.AutoFillRequired')}
</Button>
<Button
type="button"
size="sm"
variant="solid"
icon={<FaArrowRight />}
onClick={continueToNext}
>
{translate('::App.Platform.Next')} {translate('::App.Platform.Next')}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
</div>
) )
} }

View file

@ -16,7 +16,7 @@ import {
import { Field, FieldProps, Form, Formik } from 'formik' import { Field, FieldProps, Form, Formik } from 'formik'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import type { FormEvent } from 'react' import type { FormEvent } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa' import { FaArrowLeft, FaArrowRight, FaMagic } from 'react-icons/fa'
import { WorkflowDesigner } from '../workflow/WorkflowDesigner' import { WorkflowDesigner } from '../workflow/WorkflowDesigner'
import { IdentityUserDto } from '@/proxy/admin/models' import { IdentityUserDto } from '@/proxy/admin/models'
@ -39,6 +39,57 @@ type PendingLink = {
type RequiredWorkflowField = 'approvalUserFieldName' | 'approvalStatusFieldName' type RequiredWorkflowField = 'approvalUserFieldName' | 'approvalStatusFieldName'
/**
* Onay akışı kurulduğunda şart olan alanlar ve otomatik doldurmada öncelik sırasına göre
* denenen kolon adı parçaları. Bu ikisi boşsa akış çalışma zamanında kaydı kimseye düşüremez.
*/
const REQUIRED_FIELDS: Array<{ name: RequiredWorkflowField; hints: string[] }> = [
{
name: 'approvalUserFieldName',
hints: [
'approvaluser',
'onaykullanicisi',
'onaylayan',
'approvedby',
'approver',
'onaykullanici',
'kullanici',
'username',
],
},
{
name: 'approvalStatusFieldName',
hints: ['approvalstatus', 'onaydurumu', 'onaydurum', 'approvalstate', 'status', 'durum'],
},
]
const normalize = (value: string) =>
value
.toLowerCase()
.replace(/ı/g, 'i')
.replace(/ş/g, 's')
.replace(/ğ/g, 'g')
.replace(/ü/g, 'u')
.replace(/ö/g, 'o')
.replace(/ç/g, 'c')
.replace(/[^a-z0-9]/g, '')
/** Eşleşme yoksa -1; aksi halde kolonun alana ne kadar uyduğunu gösteren skor. */
const scoreColumn = (columnName: string, hints: string[]) => {
const name = normalize(columnName)
let best = -1
hints.forEach((hint, index) => {
const needle = normalize(hint)
let score = -1
if (name === needle) score = 100
else if (name.endsWith(needle)) score = 70
else if (name.startsWith(needle)) score = 60
else if (name.includes(needle)) score = 40
if (score >= 0 && score - index > best) best = score - index
})
return best
}
const toDesignerCriteria = (items: ListFormWorkflowCriteriaDto[]): WorkflowCriteriaDto[] => const toDesignerCriteria = (items: ListFormWorkflowCriteriaDto[]): WorkflowCriteriaDto[] =>
items.map((item) => ({ items.map((item) => ({
...item, ...item,
@ -76,6 +127,7 @@ function WizardStepWorkflow({
const [workflowErrors, setWorkflowErrors] = useState< const [workflowErrors, setWorkflowErrors] = useState<
Partial<Record<RequiredWorkflowField, string>> Partial<Record<RequiredWorkflowField, string>>
>({}) >({})
const [autoFillMessage, setAutoFillMessage] = useState('')
const canvasRef = useRef<HTMLDivElement | null>(null) const canvasRef = useRef<HTMLDivElement | null>(null)
const currentCriteria = useMemo(() => toDesignerCriteria(criteria), [criteria]) const currentCriteria = useMemo(() => toDesignerCriteria(criteria), [criteria])
@ -272,6 +324,41 @@ function WizardStepWorkflow({
]) ])
} }
const autoFillRequired = () => {
const used = new Set<string>()
const filled: Partial<Record<RequiredWorkflowField, string>> = {}
let unmatched = false
REQUIRED_FIELDS.forEach((field) => {
const existing = workflow[field.name]?.trim()
if (existing) {
used.add(existing)
return
}
let bestColumn = ''
let bestScore = -1
selectCommandColumns.forEach((column) => {
if (used.has(column.columnName)) return
const score = scoreColumn(column.columnName, field.hints)
if (score > bestScore) {
bestScore = score
bestColumn = column.columnName
}
})
if (bestScore < 0) {
unmatched = true
return
}
used.add(bestColumn)
filled[field.name] = bestColumn
})
onWorkflowChange({ ...workflow, ...filled, criteria })
setWorkflowErrors({})
setAutoFillMessage(unmatched ? translate('::App.ListFormWizard.AutoFillUnmatched') : '')
}
const continueToTodo = () => { const continueToTodo = () => {
if (criteria.length === 0) { if (criteria.length === 0) {
setWorkflowErrors({}) setWorkflowErrors({})
@ -302,7 +389,7 @@ function WizardStepWorkflow({
> >
{({ values }) => ( {({ values }) => (
<Form> <Form>
<Card className="mb-4" header={translate('::App.ListFormEdit.TabWorkflow')}> <Card className="mb-4">
<FormContainer> <FormContainer>
<div className="grid grid-cols-1 md:grid-cols-6 gap-4"> <div className="grid grid-cols-1 md:grid-cols-6 gap-4">
{[ {[
@ -334,7 +421,7 @@ function WizardStepWorkflow({
<Select <Select
field={field} field={field}
form={form} form={form}
isClearable isClearable={true}
options={columnOptions} options={columnOptions}
value={columnOptions.find( value={columnOptions.find(
(option) => option.value === (values as any)[name], (option) => option.value === (values as any)[name],
@ -468,7 +555,17 @@ function WizardStepWorkflow({
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}> <Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
{translate('::App.Platform.Back')} {translate('::App.Platform.Back')}
</Button> </Button>
{autoFillMessage && <span className="text-sm text-amber-600">{autoFillMessage}</span>}
<div className="flex-1 flex items-center justify-end gap-3"> <div className="flex-1 flex items-center justify-end gap-3">
<Button
size="sm"
type="button"
icon={<FaMagic />}
disabled={selectCommandColumns.length === 0}
onClick={autoFillRequired}
>
{translate('::App.ListFormWizard.AutoFillRequired')}
</Button>
<Button <Button
size="sm" size="sm"
variant="solid" variant="solid"