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",
|
"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",
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue