Wizard AutoFill Required Fields
This commit is contained in:
parent
47d9a20998
commit
e237cacc2a
8 changed files with 816 additions and 121 deletions
|
|
@ -16248,6 +16248,24 @@
|
|||
"en": "Required",
|
||||
"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",
|
||||
"key": "App.ListFormWizard.ListFormFields",
|
||||
|
|
|
|||
|
|
@ -937,7 +937,7 @@ const FileManager = () => {
|
|||
</div>
|
||||
|
||||
<Select
|
||||
size="xs"
|
||||
size="sm"
|
||||
options={sortOptions}
|
||||
value={selectedSortOption}
|
||||
onChange={(option) => {
|
||||
|
|
@ -976,7 +976,7 @@ const FileManager = () => {
|
|||
<div className="flex items-center gap-2">
|
||||
{isHostContext ? (
|
||||
<Select
|
||||
size="xs"
|
||||
size="sm"
|
||||
isLoading={tenantsLoading}
|
||||
options={tenantOptions}
|
||||
value={selectedTenantOption}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
|
|||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||
import type { SelectBoxOption } from '@/types/shared'
|
||||
import { Field, type FieldProps, useFormikContext } from 'formik'
|
||||
import { useMemo } from 'react'
|
||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { FaArrowLeft, FaArrowRight, FaMagic } from 'react-icons/fa'
|
||||
import { gantScaleOptions } from '../edit/options'
|
||||
|
||||
interface WizardStepGanttLayoutProps {
|
||||
|
|
@ -14,6 +14,99 @@ interface WizardStepGanttLayoutProps {
|
|||
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
|
||||
* (anahtar / üst anahtar) kolonlarının eşleştirilmesi ile ölçek ve genişletme davranışı.
|
||||
|
|
@ -25,7 +118,9 @@ const WizardStepGanttLayout = ({
|
|||
onBack,
|
||||
onNext,
|
||||
}: WizardStepGanttLayoutProps) => {
|
||||
const { values } = useFormikContext<ListFormWizardDto>()
|
||||
const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
|
||||
const [errorFields, setErrorFields] = useState<RequiredField[]>([])
|
||||
const [autoFillMessage, setAutoFillMessage] = useState('')
|
||||
const fieldOptions = useMemo<SelectBoxOption[]>(
|
||||
() =>
|
||||
selectCommandColumns.map((column) => ({
|
||||
|
|
@ -35,14 +130,6 @@ const WizardStepGanttLayout = ({
|
|||
[selectCommandColumns],
|
||||
)
|
||||
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 = [
|
||||
['editingEnabled', '::App.ListFormEdit.EditingEnabled'],
|
||||
['allowTaskAdding', '::App.ListFormEdit.AllowTaskAdding'],
|
||||
|
|
@ -56,27 +143,93 @@ const WizardStepGanttLayout = ({
|
|||
['allowTaskResourceUpdating', '::App.ListFormEdit.AllowTaskResourceUpdating'],
|
||||
] 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 (
|
||||
<div className="pb-20">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<Card header={translate('::App.ListFormSchedulerOptions.BasicSettings')}>
|
||||
<div className="grid grid-cols-1 gap-x-5 md:grid-cols-2">
|
||||
{selectFields.map(([name, label]) => (
|
||||
<FormItem key={name} label={label}>
|
||||
<Field name={`ganttOptionDto.${name}`}>
|
||||
{({ field, form }: FieldProps) => (
|
||||
{REQUIRED_FIELDS.map((field) => {
|
||||
const invalid = errorFields.includes(field.name)
|
||||
|
||||
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
|
||||
field={field}
|
||||
field={formikField}
|
||||
form={form}
|
||||
isClearable
|
||||
options={fieldOptions}
|
||||
value={fieldOptions.find((fieldOption) => fieldOption.value === option[name])}
|
||||
onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? '')}
|
||||
isClearable
|
||||
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.ListFormEdit.HasItemsExpr')}>
|
||||
<Field name="ganttOptionDto.hasItemsExpr" component={Input} />
|
||||
</FormItem>
|
||||
|
|
@ -121,17 +274,41 @@ const WizardStepGanttLayout = ({
|
|||
</Card>
|
||||
</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="flex w-full items-center justify-between gap-3">
|
||||
<Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}>
|
||||
{translate('::App.Platform.Back')}
|
||||
</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')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { MenuItem } from '@/proxy/menus/menu'
|
|||
import { MenuService } from '@/services/menu.service'
|
||||
import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import CreatableSelect from 'react-select/creatable'
|
||||
import {
|
||||
FaArrowDown,
|
||||
FaArrowLeft,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
|
|||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||
import type { SelectBoxOption } from '@/types/shared'
|
||||
import { Field, type FieldProps, useFormikContext } from 'formik'
|
||||
import { useMemo } from 'react'
|
||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { FaArrowLeft, FaArrowRight, FaMagic } from 'react-icons/fa'
|
||||
import { firstDayOfWeekOptions, schedulerViewOptions } from '../edit/options'
|
||||
|
||||
interface WizardStepSchedulerLayoutProps {
|
||||
|
|
@ -14,6 +14,88 @@ interface WizardStepSchedulerLayoutProps {
|
|||
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
|
||||
* 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,
|
||||
onNext,
|
||||
}: WizardStepSchedulerLayoutProps) => {
|
||||
const { values } = useFormikContext<ListFormWizardDto>()
|
||||
const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
|
||||
const [errorFields, setErrorFields] = useState<RequiredField[]>([])
|
||||
const [autoFillMessage, setAutoFillMessage] = useState('')
|
||||
const fieldOptions = useMemo<SelectBoxOption[]>(
|
||||
() =>
|
||||
selectCommandColumns.map((column) => ({
|
||||
|
|
@ -34,14 +118,6 @@ const WizardStepSchedulerLayout = ({
|
|||
[selectCommandColumns],
|
||||
)
|
||||
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 = [
|
||||
['showAllDayPanel', '::App.ListFormSchedulerOptions.ShowAllDayPanel'],
|
||||
['crossScrollingEnabled', '::App.ListFormSchedulerOptions.CrossScrollingEnabled'],
|
||||
|
|
@ -52,26 +128,97 @@ const WizardStepSchedulerLayout = ({
|
|||
['allowDeleting', '::App.ListFormSchedulerOptions.AllowDeleting'],
|
||||
] 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 (
|
||||
<div className="pb-20">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<Card header={translate('::App.ListFormSchedulerOptions.BasicSettings')}>
|
||||
{fieldMappings.slice(0, 4).map(([name, translationKey]) => (
|
||||
<FormItem key={name} label={translate(translationKey)}>
|
||||
<Field name={`schedulerOptionDto.${name}`}>
|
||||
{REQUIRED_FIELDS.map((field) => {
|
||||
const invalid = errorFields.includes(field.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) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable
|
||||
isClearable={true}
|
||||
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 ?? '')}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<Card header={translate('::App.ListFormSchedulerOptions.ViewSettings')}>
|
||||
|
|
@ -81,6 +228,7 @@ const WizardStepSchedulerLayout = ({
|
|||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable={true}
|
||||
options={schedulerViewOptions}
|
||||
value={schedulerViewOptions.find(
|
||||
(viewOption) => viewOption.value === option.defaultView,
|
||||
|
|
@ -96,6 +244,7 @@ const WizardStepSchedulerLayout = ({
|
|||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable={true}
|
||||
options={firstDayOfWeekOptions}
|
||||
value={firstDayOfWeekOptions.find(
|
||||
(dayOption) => dayOption.value === option.firstDayOfWeek,
|
||||
|
|
@ -128,14 +277,14 @@ const WizardStepSchedulerLayout = ({
|
|||
</Card>
|
||||
|
||||
<Card header={translate('::App.ListFormSchedulerOptions.RecurrenceSettings')}>
|
||||
{fieldMappings.slice(4).map(([name, translationKey]) => (
|
||||
{RECURRENCE_FIELDS.map(([name, translationKey]) => (
|
||||
<FormItem key={name} label={translate(translationKey)}>
|
||||
<Field name={`schedulerOptionDto.${name}`}>
|
||||
{({ field, form }: FieldProps) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable
|
||||
isClearable={true}
|
||||
options={fieldOptions}
|
||||
value={fieldOptions.find((fieldOption) => fieldOption.value === option[name])}
|
||||
onChange={(selected) => form.setFieldValue(field.name, selected?.value ?? '')}
|
||||
|
|
@ -155,17 +304,41 @@ const WizardStepSchedulerLayout = ({
|
|||
</Card>
|
||||
</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="flex w-full items-center justify-between gap-3">
|
||||
<Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}>
|
||||
{translate('::App.Platform.Back')}
|
||||
</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')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
|||
import type { SelectBoxOption } from '@/types/shared'
|
||||
import { Field, type FieldProps, useFormikContext } from 'formik'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
||||
import { FaArrowLeft, FaArrowRight, FaMagic } from 'react-icons/fa'
|
||||
|
||||
interface WizardStepTodoLayoutProps {
|
||||
selectedColumns: Set<string>
|
||||
|
|
@ -15,6 +15,27 @@ interface WizardStepTodoLayoutProps {
|
|||
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<{
|
||||
name: keyof TodoOptionDto
|
||||
translationKey: string
|
||||
|
|
@ -59,6 +80,42 @@ const mappings: Array<{
|
|||
{ 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
|
||||
* ihtiyaç duyduğu kolon eşleşmeleri. Yalnızca Todo layout'u seçiliyken gösterilir.
|
||||
|
|
@ -72,19 +129,54 @@ const WizardStepTodoLayout = ({
|
|||
}: WizardStepTodoLayoutProps) => {
|
||||
const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
|
||||
const [validationError, setValidationError] = useState('')
|
||||
const fieldOptions = useMemo<SelectBoxOption[]>(
|
||||
() =>
|
||||
selectCommandColumns
|
||||
.filter((column) => selectedColumns.has(column.columnName))
|
||||
.map((column) => ({ value: column.columnName, label: column.columnName })),
|
||||
const [autoFillMessage, setAutoFillMessage] = useState('')
|
||||
// Todo kartları yalnızca ekrana alınan kolonları gösterebilir; eşleşmeler de onlardan seçilir.
|
||||
const availableColumns = useMemo(
|
||||
() => selectCommandColumns.filter((column) => selectedColumns.has(column.columnName)),
|
||||
[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 = () => {
|
||||
if (
|
||||
values.todo &&
|
||||
(!values.todoOptionDto.titleExpr?.trim() || !values.todoOptionDto.statusExpr?.trim())
|
||||
) {
|
||||
const missing = REQUIRED_FIELDS.some((field) => !values.todoOptionDto[field.name]?.trim())
|
||||
if (values.todo && missing) {
|
||||
setValidationError(translate('::App.ListFormFieldEdit.TodoRequiredFields'))
|
||||
return
|
||||
}
|
||||
|
|
@ -147,6 +239,7 @@ const WizardStepTodoLayout = ({
|
|||
</FormItem>
|
||||
</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>}
|
||||
|
||||
<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}>
|
||||
{translate('::App.Platform.Back')}
|
||||
</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
|
||||
size="sm"
|
||||
variant="solid"
|
||||
|
|
@ -166,6 +269,7 @@ const WizardStepTodoLayout = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import type { ListFormWizardDto } from '@/proxy/admin/wizard/models'
|
|||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||
import type { SelectBoxOption } from '@/types/shared'
|
||||
import { Field, type FieldProps, useFormikContext } from 'formik'
|
||||
import { useMemo } from 'react'
|
||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { FaArrowLeft, FaArrowRight, FaMagic } from 'react-icons/fa'
|
||||
|
||||
interface WizardStepTreeLayoutProps {
|
||||
selectCommandColumns: DatabaseColumnDto[]
|
||||
|
|
@ -13,6 +13,53 @@ interface WizardStepTreeLayoutProps {
|
|||
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
|
||||
* kolonlarının eşleştirilmesi. Yalnızca Tree layout'u seçiliyken gösterilir.
|
||||
|
|
@ -23,7 +70,9 @@ const WizardStepTreeLayout = ({
|
|||
onBack,
|
||||
onNext,
|
||||
}: WizardStepTreeLayoutProps) => {
|
||||
const { values } = useFormikContext<ListFormWizardDto>()
|
||||
const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
|
||||
const [errorFields, setErrorFields] = useState<RequiredField[]>([])
|
||||
const [autoFillMessage, setAutoFillMessage] = useState('')
|
||||
const fieldOptions = useMemo<SelectBoxOption[]>(
|
||||
() =>
|
||||
selectCommandColumns.map((column) => ({
|
||||
|
|
@ -33,38 +82,92 @@ const WizardStepTreeLayout = ({
|
|||
[selectCommandColumns],
|
||||
)
|
||||
|
||||
const selectFields = [
|
||||
{
|
||||
name: 'treeOptionDto.keyExpr',
|
||||
value: values.treeOptionDto.keyExpr,
|
||||
label: translate('::App.Listform.ListformField.KeyFieldName'),
|
||||
},
|
||||
{
|
||||
name: 'treeOptionDto.parentIdExpr',
|
||||
value: values.treeOptionDto.parentIdExpr,
|
||||
label: translate('::App.ListFormEdit.ParentIdExpr'),
|
||||
},
|
||||
]
|
||||
const autoFillRequired = () => {
|
||||
const option = values.treeOptionDto
|
||||
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(`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 (
|
||||
<div className="pb-20">
|
||||
<div className="grid grid-cols-1 gap-x-6 md:grid-cols-2">
|
||||
{selectFields.map((item) => (
|
||||
<FormItem key={item.name} label={item.label}>
|
||||
<Field name={item.name}>
|
||||
{REQUIRED_FIELDS.map((item) => {
|
||||
const invalid = errorFields.includes(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) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable
|
||||
isClearable={true}
|
||||
options={fieldOptions}
|
||||
value={fieldOptions.find((option) => option.value === item.value)}
|
||||
onChange={(option) => form.setFieldValue(field.name, option?.value ?? '')}
|
||||
value={fieldOptions.find(
|
||||
(option) => option.value === values.treeOptionDto[item.name],
|
||||
)}
|
||||
onChange={(option) => {
|
||||
form.setFieldValue(field.name, option?.value ?? '')
|
||||
setErrorFields((current) => current.filter((name) => name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
|
||||
<FormItem label={translate('::App.ListFormEdit.HasItemsExpr')}>
|
||||
<Field name="treeOptionDto.hasItemsExpr" component={Input} />
|
||||
|
|
@ -80,17 +183,41 @@ const WizardStepTreeLayout = ({
|
|||
</FormItem>
|
||||
</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="flex w-full items-center justify-between gap-3">
|
||||
<Button type="button" size="sm" icon={<FaArrowLeft />} onClick={onBack}>
|
||||
{translate('::App.Platform.Back')}
|
||||
</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')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||
import { useEffect, useMemo, useRef, useState } 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 { IdentityUserDto } from '@/proxy/admin/models'
|
||||
|
||||
|
|
@ -39,6 +39,57 @@ type PendingLink = {
|
|||
|
||||
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[] =>
|
||||
items.map((item) => ({
|
||||
...item,
|
||||
|
|
@ -76,6 +127,7 @@ function WizardStepWorkflow({
|
|||
const [workflowErrors, setWorkflowErrors] = useState<
|
||||
Partial<Record<RequiredWorkflowField, string>>
|
||||
>({})
|
||||
const [autoFillMessage, setAutoFillMessage] = useState('')
|
||||
const canvasRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
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 = () => {
|
||||
if (criteria.length === 0) {
|
||||
setWorkflowErrors({})
|
||||
|
|
@ -302,7 +389,7 @@ function WizardStepWorkflow({
|
|||
>
|
||||
{({ values }) => (
|
||||
<Form>
|
||||
<Card className="mb-4" header={translate('::App.ListFormEdit.TabWorkflow')}>
|
||||
<Card className="mb-4">
|
||||
<FormContainer>
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
{[
|
||||
|
|
@ -334,7 +421,7 @@ function WizardStepWorkflow({
|
|||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable
|
||||
isClearable={true}
|
||||
options={columnOptions}
|
||||
value={columnOptions.find(
|
||||
(option) => option.value === (values as any)[name],
|
||||
|
|
@ -468,7 +555,17 @@ function WizardStepWorkflow({
|
|||
<Button size="sm" variant="default" type="button" icon={<FaArrowLeft />} onClick={onBack}>
|
||||
{translate('::App.Platform.Back')}
|
||||
</Button>
|
||||
{autoFillMessage && <span className="text-sm text-amber-600">{autoFillMessage}</span>}
|
||||
<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
|
||||
size="sm"
|
||||
variant="solid"
|
||||
|
|
|
|||
Loading…
Reference in a new issue