Helper Codes güncellemeleri ve EditorScript düzenlemesi

This commit is contained in:
Sedat Öztürk 2026-05-31 12:36:51 +03:00
parent 96f7091d46
commit e1c808310d
6 changed files with 863 additions and 1231 deletions

View file

@ -26,14 +26,11 @@ const boolOptions = [
{ key: 'readOnly', label: 'readOnly' },
{ key: 'disabled', label: 'disabled' },
{ key: 'searchEnabled', label: 'searchEnabled' },
{ key: 'showDataBeforeSearch', label: 'showDataBeforeSearch' },
{ key: 'acceptCustomValue', label: 'acceptCustomValue' },
{ key: 'showSpinButtons', label: 'showSpinButtons' },
{ key: 'useMaskBehavior', label: 'useMaskBehavior' },
{ key: 'useMaskedValue', label: 'useMaskedValue' },
{ key: 'spellcheck', label: 'spellcheck' },
{ key: 'openOnFieldClick', label: 'openOnFieldClick' },
{ key: 'showDropDownButton', label: 'showDropDownButton' },
]
const htmlToolbarItems = [
@ -302,10 +299,6 @@ function EditorOptionsBuilderDialog({
>
Editor Options Builder
</h5>
<FaSlidersH
className="text-gray-400"
title="Bu dialog editorOptions alanını metin yazmadan oluşturmak için kullanılır."
/>
</div>
{parseError && (
@ -314,8 +307,8 @@ function EditorOptionsBuilderDialog({
</div>
)}
<div className="grid min-h-0 grid-cols-1 gap-4 overflow-y-auto pr-1 lg:grid-cols-3">
<section className="lg:col-span-2 flex flex-col gap-4">
<div className="grid grid-cols-2 min-h-0 gap-4 overflow-y-auto pr-1 ">
<section className="flex flex-col gap-4">
<div className="rounded border border-gray-200 dark:border-gray-700 p-3">
<div
className="mb-3 text-sm font-semibold"
@ -327,7 +320,7 @@ function EditorOptionsBuilderDialog({
{boolOptions.map((option) => (
<label
key={option.key}
className="flex items-center gap-2 rounded border border-gray-100 dark:border-gray-700 px-2 py-2 text-sm"
className="flex items-center gap-2 rounded px-1 py-1 text-sm"
title={`${option.key}: true/false olarak editorOptions içine yazılır.`}
>
<input
@ -565,7 +558,7 @@ function EditorOptionsBuilderDialog({
>
5. NumberBox
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div className="grid grid-cols-1 md:grid-cols-5 gap-3">
{['min', 'max', 'step'].map((key) => (
<label key={key} className="text-xs text-gray-500">
{key}
@ -585,7 +578,7 @@ function EditorOptionsBuilderDialog({
</label>
))}
<label className="text-xs text-gray-500">
format.type
type
<select
className={baseInputClass}
value={toSelectValue(getByPath(options, 'format.type'))}
@ -600,7 +593,7 @@ function EditorOptionsBuilderDialog({
</select>
</label>
<label className="text-xs text-gray-500">
format.precision
precision
<input
className={baseInputClass}
value={toInputValue(getByPath(options, 'format.precision'))}
@ -706,12 +699,106 @@ function EditorOptionsBuilderDialog({
</div>
</div>
<div className="rounded border border-gray-200 dark:border-gray-700 p-3">
<div className="flex items-center justify-between gap-2 mb-3">
<div>
<div
className="text-sm font-semibold"
title="DevExtreme dokümanındaki herhangi bir editorOptions path'ini eklemek için kullan. Örnek path: toolbar.multiline, maskRules.X, inputAttr.aria-label."
>
7. Özel Ayarlar
</div>
</div>
<Button
type="button"
size="sm"
icon={<FaPlus />}
onClick={() =>
setCustomOptions((current) => [
...current,
{
id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
path: '',
value: '',
type: 'string',
},
])
}
>
Ekle
</Button>
</div>
<div className="flex flex-col gap-2">
{customOptions.map((option) => (
<div key={option.id} className="grid grid-cols-12 gap-2">
<input
className={`${baseInputClass} col-span-4`}
value={option.path}
onChange={(event) =>
setCustomOptions((current) =>
current.map((item) =>
item.id === option.id ? { ...item, path: event.target.value } : item,
),
)
}
placeholder="path.to.option"
title="Nokta ile nested path yaz. Örnek: toolbar.multiline"
/>
<select
className={`${baseInputClass} col-span-2`}
value={option.type}
onChange={(event) =>
setCustomOptions((current) =>
current.map((item) =>
item.id === option.id
? { ...item, type: event.target.value as CustomOption['type'] }
: item,
),
)
}
>
<option value="string">string</option>
<option value="number">number</option>
<option value="boolean">boolean</option>
<option value="json">json</option>
</select>
<input
className={`${baseInputClass} col-span-5`}
value={option.value}
onChange={(event) =>
setCustomOptions((current) =>
current.map((item) =>
item.id === option.id ? { ...item, value: event.target.value } : item,
),
)
}
placeholder={option.type === 'boolean' ? 'true / false' : 'value'}
title="Tip JSON ise object/array yazabilirsin. Tip boolean ise true veya false yaz."
/>
<Button
type="button"
shape="circle"
variant="plain"
icon={<FaTrash />}
onClick={() =>
setCustomOptions((current) =>
current.filter((item) => item.id !== option.id),
)
}
/>
</div>
))}
</div>
</div>
</section>
<section className="rounded border border-gray-200 dark:border-gray-700 p-3 flex flex-col min-h-[420px]">
<div className="rounded border border-gray-200 dark:border-gray-700 p-3">
<div
className="mb-3 text-sm font-semibold"
title="Daha önce kullandığın hazır editorOptions örnekleri. Tıkladığın preset mevcut JSON ile birleştirilir, yani başka seçenekleri silmeden üzerine ekler."
>
7. Hazır Ayarlar
Hazır Ayarlar
</div>
<div className="flex flex-wrap gap-2">
<Button
@ -886,108 +973,14 @@ function EditorOptionsBuilderDialog({
</div>
</div>
<div className="rounded border border-gray-200 dark:border-gray-700 p-3">
<div className="flex items-center justify-between gap-2 mb-3">
<div>
<div
className="text-sm font-semibold"
title="DevExtreme dokümanındaki herhangi bir editorOptions path'ini eklemek için kullan. Örnek path: toolbar.multiline, maskRules.X, inputAttr.aria-label."
>
8. Özel Ayarlar
</div>
</div>
<Button
type="button"
size="sm"
icon={<FaPlus />}
onClick={() =>
setCustomOptions((current) => [
...current,
{
id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
path: '',
value: '',
type: 'string',
},
])
}
>
Ekle
</Button>
</div>
<div className="flex flex-col gap-2">
{customOptions.map((option) => (
<div key={option.id} className="grid grid-cols-12 gap-2">
<input
className={`${baseInputClass} col-span-4`}
value={option.path}
onChange={(event) =>
setCustomOptions((current) =>
current.map((item) =>
item.id === option.id ? { ...item, path: event.target.value } : item,
),
)
}
placeholder="path.to.option"
title="Nokta ile nested path yaz. Örnek: toolbar.multiline"
/>
<select
className={`${baseInputClass} col-span-2`}
value={option.type}
onChange={(event) =>
setCustomOptions((current) =>
current.map((item) =>
item.id === option.id
? { ...item, type: event.target.value as CustomOption['type'] }
: item,
),
)
}
>
<option value="string">string</option>
<option value="number">number</option>
<option value="boolean">boolean</option>
<option value="json">json</option>
</select>
<input
className={`${baseInputClass} col-span-5`}
value={option.value}
onChange={(event) =>
setCustomOptions((current) =>
current.map((item) =>
item.id === option.id ? { ...item, value: event.target.value } : item,
),
)
}
placeholder={option.type === 'boolean' ? 'true / false' : 'value'}
title="Tip JSON ise object/array yazabilirsin. Tip boolean ise true veya false yaz."
/>
<Button
type="button"
shape="circle"
variant="plain"
icon={<FaTrash />}
onClick={() =>
setCustomOptions((current) =>
current.filter((item) => item.id !== option.id),
)
}
/>
</div>
))}
</div>
</div>
</section>
<section className="rounded border border-gray-200 dark:border-gray-700 p-3 flex flex-col min-h-[420px]">
<div
className="flex items-center gap-2 text-sm font-semibold mb-3"
className="flex items-center gap-2 text-sm font-semibold mt-3"
title="Kaydet/Uygula sonrası editorOptions alanına yazılacak net JSON budur."
>
<FaCode />
JSON Önizleme
</div>
<pre className="flex-1 overflow-auto rounded bg-gray-50 dark:bg-gray-900 p-3 text-xs whitespace-pre-wrap">
<pre className="flex overflow-auto rounded bg-gray-50 dark:bg-gray-900 p-3 text-xs whitespace-pre-wrap">
{preview || '{}'}
</pre>
</section>

View file

@ -1,6 +1,6 @@
import { Button, Dialog } from '@/components/ui'
import { SelectBoxOption } from '@/types/shared'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Editor from '@monaco-editor/react'
import { useEffect, useMemo, useState } from 'react'
import { FaBolt, FaCheck, FaCode, FaPlus, FaTimes, FaTrash } from 'react-icons/fa'
@ -10,16 +10,6 @@ type CopyMapping = {
target: string
}
type ToggleRule = {
id: string
target: string
property: 'visible' | 'disabled' | 'readOnly'
source: string
operator: 'equals' | 'notEquals' | 'empty' | 'notEmpty'
value: string
whenTrue: boolean
}
type ConditionalAction = {
id: string
source: string
@ -35,18 +25,13 @@ type ConditionalAction = {
value: string
actionType:
| 'setField'
| 'copySelected'
| 'setFieldState'
| 'setFieldStyle'
| 'apiToField'
| 'openUrl'
| 'alert'
| 'confirm'
| 'calculate'
targetField: string
targetProperty: 'visible' | 'disabled' | 'readOnly' | 'color' | 'backgroundColor' | 'borderColor'
textValue: string
selectedColumn: string
apiUrl: string
apiMethod: 'GET' | 'POST'
responsePath: string
@ -76,9 +61,7 @@ const createConditionalAction = (): ConditionalAction => ({
value: '',
actionType: 'setField',
targetField: '',
targetProperty: 'disabled',
textValue: '',
selectedColumn: '',
apiUrl: '',
apiMethod: 'GET',
responsePath: '',
@ -98,14 +81,6 @@ function quote(value: string) {
return JSON.stringify(value ?? '')
}
function buildCondition(rule: ToggleRule) {
const source = `next[${quote(rule.source)}]`
if (rule.operator === 'empty') return `!${source}`
if (rule.operator === 'notEmpty') return `!!${source}`
if (rule.operator === 'notEquals') return `${source} !== ${quote(rule.value)}`
return `${source} === ${quote(rule.value)}`
}
function buildGenericCondition(rule: ConditionalAction) {
if (rule.operator === 'always') return 'true'
const source = `next[${quote(rule.source)}]`
@ -119,11 +94,11 @@ function buildGenericCondition(rule: ConditionalAction) {
return `${source} === ${quote(rule.value)}`
}
function conditionNeedsSource(operator: ConditionalAction['operator'] | ToggleRule['operator']) {
function conditionNeedsSource(operator: ConditionalAction['operator']) {
return operator !== 'always'
}
function conditionNeedsValue(operator: ConditionalAction['operator'] | ToggleRule['operator']) {
function conditionNeedsValue(operator: ConditionalAction['operator']) {
return (
operator === 'equals' ||
operator === 'notEquals' ||
@ -138,11 +113,6 @@ function isConditionalActionReady(action: ConditionalAction) {
if (conditionNeedsValue(action.operator) && !action.value.trim()) return false
if (action.actionType === 'setField')
return Boolean(action.targetField && action.textValue.trim())
if (action.actionType === 'copySelected')
return Boolean(action.targetField && action.selectedColumn)
if (action.actionType === 'setFieldState') return Boolean(action.targetField)
if (action.actionType === 'setFieldStyle')
return Boolean(action.targetField && action.textValue.trim())
if (action.actionType === 'apiToField') return Boolean(action.targetField && action.apiUrl.trim())
if (action.actionType === 'openUrl') return Boolean(action.textValue.trim())
if (action.actionType === 'alert' || action.actionType === 'confirm')
@ -151,20 +121,6 @@ function isConditionalActionReady(action: ConditionalAction) {
return false
}
function isToggleRuleReady(rule: ToggleRule) {
if (!rule.target || !rule.source) return false
if (conditionNeedsValue(rule.operator) && !rule.value.trim()) return false
return true
}
function buildValueExpression(value: string) {
const trimmed = value.trim()
if (!trimmed) return "''"
if (trimmed === 'true' || trimmed === 'false') return trimmed
if (trimmed.startsWith('=') && trimmed.length > 1) return trimmed.slice(1)
return quote(value)
}
function fieldLabel(value: string, fallback: string) {
return value || fallback
}
@ -172,7 +128,6 @@ function fieldLabel(value: string, fallback: string) {
function buildScript({
currentField,
copyMappings,
toggleRules,
daysStartField,
daysEndField,
daysTargetField,
@ -188,11 +143,9 @@ function buildScript({
timeTargetField,
conditionalActions,
serviceCall,
customScript,
}: {
currentField?: string
copyMappings: CopyMapping[]
toggleRules: ToggleRule[]
daysStartField: string
daysEndField: string
daysTargetField: string
@ -208,12 +161,9 @@ function buildScript({
timeTargetField: string
conditionalActions: ConditionalAction[]
serviceCall: string
customScript: string
}) {
const activeCopyMappings = copyMappings.filter((mapping) => mapping.source && mapping.target)
const activeToggleRules = toggleRules.filter(isToggleRuleReady)
const hasCopyMappings = activeCopyMappings.length > 0
const hasToggleRules = activeToggleRules.length > 0
const hasDateDifference = Boolean(daysStartField && daysEndField && daysTargetField)
const hasSelectedItemAmount = Boolean(
selectedItemAmountEnabled && amountQuantityField && amountUnitPriceField && amountTotalField,
@ -229,7 +179,6 @@ function buildScript({
const hasServiceCall = Boolean(serviceCall.trim())
const hasBuilderSelection =
hasCopyMappings ||
hasToggleRules ||
hasDateDifference ||
hasSelectedItemAmount ||
hasRowAmount ||
@ -238,7 +187,7 @@ function buildScript({
hasServiceCall
if (!hasBuilderSelection) {
return customScript.trim()
return ''
}
const needsSetFormData =
@ -248,17 +197,15 @@ function buildScript({
hasRowAmount ||
hasTimeDifference ||
activeConditionalActions.some((action) =>
['setField', 'copySelected', 'apiToField', 'calculate'].includes(action.actionType),
['setField', 'apiToField', 'calculate'].includes(action.actionType),
)
const needsSelectedItem =
hasCopyMappings ||
hasSelectedItemAmount ||
activeConditionalActions.some((action) =>
[
'copySelected',
'calculate',
'setField',
'setFieldStyle',
'apiToField',
'openUrl',
'alert',
@ -269,30 +216,21 @@ function buildScript({
activeCopyMappings.some((mapping) => mapping.source.includes('.')) ||
activeConditionalActions.some(
(action) =>
action.actionType === 'copySelected' ||
action.actionType === 'apiToField' ||
['setField', 'setFieldStyle', 'openUrl', 'alert', 'confirm'].includes(action.actionType),
['setField', 'openUrl', 'alert', 'confirm'].includes(action.actionType),
)
const needsRenderTemplate = activeConditionalActions.some((action) =>
['setField', 'setFieldStyle', 'apiToField', 'openUrl', 'alert', 'confirm'].includes(
['setField', 'apiToField', 'openUrl', 'alert', 'confirm'].includes(
action.actionType,
),
)
const needsForm =
hasToggleRules ||
activeConditionalActions.some(
(action) => action.actionType === 'setFieldState' || action.actionType === 'setFieldStyle',
)
const needsFieldState =
hasToggleRules ||
activeConditionalActions.some((action) => action.actionType === 'setFieldState')
const needsFieldStyle = activeConditionalActions.some(
(action) => action.actionType === 'setFieldStyle',
const needsRollbackCurrentValue = activeConditionalActions.some(
(action) => action.actionType === 'confirm',
)
const lines: string[] = ['(async () => {']
if (needsSetFormData || hasToggleRules || hasConditionalActions) {
if (needsSetFormData || hasConditionalActions) {
lines.push(
" const currentField = (typeof editor !== 'undefined' && editor?.dataField) || e?.dataField || " +
quote(currentField || '') +
@ -301,6 +239,12 @@ function buildScript({
lines.push(' const next = { ...formData, [currentField]: e?.value };')
}
if (needsRollbackCurrentValue) {
lines.push(
' const rollbackCurrentValue = () => { const previousFieldValue = typeof previousValue !== "undefined" ? previousValue : (e?.previousValue ?? formData?.[currentField] ?? null); next[currentField] = previousFieldValue; const currentEditor = e?.component?.getEditor?.(currentField); currentEditor?.option?.("value", previousFieldValue); if (!currentEditor) e?.component?.option?.("value", previousFieldValue); if (typeof setFormData === "function") setFormData({ ...next, [currentField]: previousFieldValue }); };',
)
}
if (needsSelectedItem) {
lines.push(
' const selectedItem = e?.component?.option ? e.component.option("selectedItem") : null;',
@ -319,24 +263,6 @@ function buildScript({
)
}
if (needsForm) {
lines.push(
' const form = e?.component?.itemOption ? e.component : ((typeof editor !== "undefined" && editor?.component?.option) ? editor.component.option("editing.form") : null);',
)
}
if (needsFieldState) {
lines.push(
' const setFieldState = (field, prop, flag) => { if (!field) return; if (prop === "visible") { form?.itemOption?.(field, "visible", flag); return; } form?.getEditor?.(field)?.option(prop, flag); const item = form?.itemOption?.(field) || {}; form?.itemOption?.(field, "editorOptions", { ...(item.editorOptions || {}), [prop]: flag }); };',
)
}
if (needsFieldStyle) {
lines.push(
' const setFieldStyle = (field, prop, value) => { const editorInstance = form?.getEditor?.(field); const element = editorInstance?.element?.(); const node = element?.get ? element.get(0) : element; if (node?.style) node.style[prop] = value || ""; const input = node?.querySelector?.("input, textarea, .dx-texteditor-input"); if (input?.style) input.style[prop] = value || ""; };',
)
}
activeCopyMappings.forEach((mapping) => {
const source = mapping.source.includes('.')
? `getByPath(selectedItem, ${quote(mapping.source)})`
@ -421,30 +347,6 @@ function buildScript({
)
}
if (action.actionType === 'copySelected' && action.targetField && action.selectedColumn) {
lines.push(
` next[${quote(action.targetField)}] = selectedItem ? getByPath(selectedItem, ${quote(
action.selectedColumn,
)}) : next[${quote(action.targetField)}];`,
)
}
if (action.actionType === 'setFieldState' && action.targetField) {
lines.push(
` setFieldState(${quote(action.targetField)}, ${quote(action.targetProperty)}, ${buildValueExpression(
action.textValue || 'true',
)});`,
)
}
if (action.actionType === 'setFieldStyle' && action.targetField && action.textValue) {
lines.push(
` setFieldStyle(${quote(action.targetField)}, ${quote(
action.targetProperty,
)}, renderTemplate(${quote(action.textValue)}));`,
)
}
if (action.actionType === 'apiToField' && action.apiUrl && action.targetField) {
const method = action.apiMethod || 'GET'
lines.push(` const apiUrl = renderTemplate(${quote(action.apiUrl)});`)
@ -476,7 +378,11 @@ function buildScript({
}
if (action.actionType === 'confirm' && action.message) {
lines.push(` if (!confirm(renderTemplate(${quote(action.message)}))) return;`)
lines.push(
` if (!confirm(renderTemplate(${quote(
action.message,
)}))) { rollbackCurrentValue(); return; }`,
)
}
if (action.actionType === 'calculate' && action.targetField && action.formula) {
@ -494,33 +400,10 @@ function buildScript({
lines.push(' setFormData(next);')
}
if (hasToggleRules) {
activeToggleRules.forEach((rule) => {
const condition = buildCondition(rule)
lines.push(` {`)
lines.push(` const flag = (${condition}) ? ${rule.whenTrue} : ${!rule.whenTrue};`)
if (rule.property === 'visible') {
lines.push(` setFieldState(${quote(rule.target)}, 'visible', flag);`)
} else {
lines.push(` setFieldState(${quote(rule.target)}, ${quote(rule.property)}, flag);`)
}
lines.push(` }`)
})
}
if (serviceCall.trim()) {
lines.push(` ${serviceCall.trim().replace(/;?$/, ';')}`)
}
if (customScript.trim()) {
lines.push(' // Custom script')
customScript
.split('\n')
.map((line) => line.trimEnd())
.filter(Boolean)
.forEach((line) => lines.push(` ${line}`))
}
lines.push('})();')
return lines.join('\n')
}
@ -535,7 +418,6 @@ function EditorScriptBuilderDialog({
}: EditorScriptBuilderDialogProps) {
const availableFields = useMemo(() => fieldOptions(fields), [fields])
const [copyMappings, setCopyMappings] = useState<CopyMapping[]>([])
const [toggleRules, setToggleRules] = useState<ToggleRule[]>([])
const [daysStartField, setDaysStartField] = useState('')
const [daysEndField, setDaysEndField] = useState('')
const [daysTargetField, setDaysTargetField] = useState('')
@ -551,13 +433,12 @@ function EditorScriptBuilderDialog({
const [timeTargetField, setTimeTargetField] = useState('')
const [conditionalActions, setConditionalActions] = useState<ConditionalAction[]>([])
const [serviceCall, setServiceCall] = useState('')
const [customScript, setCustomScript] = useState('')
const { translate } = useLocalization()
const [scriptEditorValue, setScriptEditorValue] = useState('')
const [lastGeneratedScript, setLastGeneratedScript] = useState('')
useEffect(() => {
if (!isOpen) return
setCopyMappings([])
setToggleRules([])
setDaysStartField('')
setDaysEndField('')
setDaysTargetField('')
@ -573,7 +454,8 @@ function EditorScriptBuilderDialog({
setTimeTargetField('')
setConditionalActions([])
setServiceCall('')
setCustomScript('')
setScriptEditorValue(value?.trim() || '')
setLastGeneratedScript('')
}, [isOpen, value])
const findField = (fieldName: string) =>
@ -592,12 +474,6 @@ function EditorScriptBuilderDialog({
setTimeTargetField((current) => current || findField('TotalHours'))
}
const fillDateDefaults = () => {
setDaysStartField((current) => current || findField('StartDate'))
setDaysEndField((current) => current || findField('EndDate'))
setDaysTargetField((current) => current || findField('TotalDays'))
}
const hydrateKnownScript = (script?: string) => {
const source = script?.trim()
if (!source) return false
@ -649,21 +525,20 @@ function EditorScriptBuilderDialog({
if (!isOpen) return
const existingScript = value?.trim()
if (!existingScript) {
setCustomScript('')
setScriptEditorValue('')
return
}
const hydrated = hydrateKnownScript(existingScript)
setCustomScript(hydrated ? '' : existingScript)
setScriptEditorValue(hydrated ? '' : existingScript)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [availableFields, isOpen, value])
const preview = useMemo(
const generatedScript = useMemo(
() =>
buildScript({
currentField,
copyMappings,
toggleRules,
daysStartField,
daysEndField,
daysTargetField,
@ -679,7 +554,6 @@ function EditorScriptBuilderDialog({
timeTargetField,
conditionalActions,
serviceCall,
customScript,
}),
[
amountQuantityField,
@ -689,7 +563,6 @@ function EditorScriptBuilderDialog({
conditionalActions,
copyMappings,
currentField,
customScript,
daysEndField,
daysStartField,
daysTargetField,
@ -700,10 +573,18 @@ function EditorScriptBuilderDialog({
timeEndField,
timeStartField,
timeTargetField,
toggleRules,
],
)
useEffect(() => {
if (!isOpen) return
setScriptEditorValue((current) => {
if (current === lastGeneratedScript || !current.trim()) return generatedScript
return current
})
setLastGeneratedScript(generatedScript)
}, [generatedScript, isOpen, lastGeneratedScript])
const renderFieldSelect = (value: string, onChange: (value: string) => void) => (
<select
className={baseInputClass}
@ -737,7 +618,7 @@ function EditorScriptBuilderDialog({
</label>
)
const needsCompareValue = (operator: ConditionalAction['operator'] | ToggleRule['operator']) =>
const needsCompareValue = (operator: ConditionalAction['operator']) =>
operator === 'equals' ||
operator === 'notEquals' ||
operator === 'contains' ||
@ -762,19 +643,6 @@ function EditorScriptBuilderDialog({
if (action.actionType === 'setField') {
return `${condition}, ${target} alanına ${action.textValue || 'değer'} yaz.`
}
if (action.actionType === 'copySelected') {
return `${condition}, seçili kayıttaki ${action.selectedColumn || 'path'} değerini ${target} alanına yaz.`
}
if (action.actionType === 'setFieldState') {
return `${condition}, ${target} alanında ${action.targetProperty} = ${
action.textValue || 'true'
} yap.`
}
if (action.actionType === 'setFieldStyle') {
return `${condition}, ${target} alanının ${action.targetProperty} stilini ${
action.textValue || 'değer'
} yap.`
}
if (action.actionType === 'apiToField') {
return `${condition}, ${action.apiMethod} ${action.apiUrl || 'api url'} çağır ve sonucu ${target} alanına yaz.`
}
@ -809,16 +677,12 @@ function EditorScriptBuilderDialog({
<h5
title={`${currentField || 'Geçerli alan'} değiştiğinde çalışacak editorScript üretir. Basit kopyalama ve hazır hesaplamalar için ilk bölümleri, API/URL/alert/özel koşul gibi esnek davranışlar için Koşullu Aksiyon bölümünü kullan.${
value?.trim()
? ' Mevcut editorScript otomatik olarak özel script alanında korunur. Yeni bir builder seçeneği eklemezsen Uygula aynı scripti geri yazar.'
? ' Mevcut editorScript sağdaki TypeScript editor içinde korunur. Yeni bir builder seçeneği eklemezsen Uygula aynı scripti geri yazar.'
: ''
}`}
>
Editor Script Builder
</h5>
<FaBolt
className="text-gray-400"
title="Bu dialog editorScript alanını seçimlerle oluşturmak için kullanılır."
/>
</div>
<div className="grid min-h-0 grid-cols-1 gap-4 overflow-y-auto pr-1 lg:grid-cols-2">
@ -1005,24 +869,18 @@ function EditorScriptBuilderDialog({
}
>
<option value="setField">Field değerini yaz</option>
<option value="copySelected">Seçili kayıttan kopyala</option>
<option value="setFieldState">Field durumunu değiştir</option>
<option value="setFieldStyle">Field stilini değiştir</option>
<option value="apiToField">API sonucunu field'a yaz</option>
<option value="openUrl">URL </option>
<option value="alert">Uyarı göster</option>
<option value="confirm">Onay iste</option>
<option value="calculate">Formül hesapla</option>
<option value="apiToField">API sonucunu field'a yaz</option>
</select>
</label>
{[
'setField',
'copySelected',
'apiToField',
'calculate',
'setFieldState',
'setFieldStyle',
].includes(action.actionType) && (
<div className="col-span-12 md:col-span-4">
{renderLabeledFieldSelect('Hedef field', action.targetField, (value) =>
@ -1043,80 +901,6 @@ function EditorScriptBuilderDialog({
/>
)}
{action.actionType === 'copySelected' && (
<input
className={`${baseInputClass} col-span-4`}
value={action.selectedColumn}
onChange={(event) =>
updateConditionalAction(action.id, {
selectedColumn: event.target.value,
})
}
placeholder="selected item path"
title="Seçilen lookup kaydından okunacak path. Örnek: UomId veya Customer.Name"
/>
)}
{action.actionType === 'setFieldState' && (
<>
<select
className={`${baseInputClass} col-span-2`}
value={action.targetProperty}
title="Hedef field'ın değiştirilecek özelliği."
onChange={(event) =>
updateConditionalAction(action.id, {
targetProperty: event.target
.value as ConditionalAction['targetProperty'],
})
}
>
<option value="disabled">disabled</option>
<option value="readOnly">readOnly</option>
<option value="visible">visible</option>
</select>
<select
className={`${baseInputClass} col-span-2`}
value={action.textValue || 'true'}
title="Koşul sağlanınca property true mu false mu olsun?"
onChange={(event) =>
updateConditionalAction(action.id, { textValue: event.target.value })
}
>
<option value="true">true</option>
<option value="false">false</option>
</select>
</>
)}
{action.actionType === 'setFieldStyle' && (
<>
<select
className={`${baseInputClass} col-span-2`}
value={action.targetProperty}
title="Koşul sağlandığında hedef field üzerinde uygulanacak CSS style property'si. Örnek: color, backgroundColor, borderColor."
onChange={(event) =>
updateConditionalAction(action.id, {
targetProperty: event.target
.value as ConditionalAction['targetProperty'],
})
}
>
<option value="color">color</option>
<option value="backgroundColor">backgroundColor</option>
<option value="borderColor">borderColor</option>
</select>
<input
className={`${baseInputClass} col-span-2`}
value={action.textValue}
onChange={(event) =>
updateConditionalAction(action.id, { textValue: event.target.value })
}
placeholder="#ef4444"
title="CSS renk değeri yaz. Örnek: red, #ef4444, rgb(239,68,68). Token da kullanabilirsin: {ColorCode}"
/>
</>
)}
{action.actionType === 'apiToField' && (
<>
<select
@ -1212,222 +996,19 @@ function EditorScriptBuilderDialog({
</div>
<div className="rounded border border-gray-200 dark:border-gray-700 p-3">
<div className="flex items-center justify-between gap-2 mb-3">
<div>
<div
className="text-sm font-semibold"
title="Bir kaynak alan belirli bir koşulu sağladığında hedef alanı disabled/readOnly/visible yapar. Örnek: PhoneNumber 1000 ise Surname disabled true."
>
3. Field Durum Kuralları
</div>
</div>
<Button
type="button"
size="sm"
icon={<FaPlus />}
title="Basit field state kuralı ekler. Örnek: kaynak alan boşsa hedef alan disabled true olsun."
onClick={() =>
setToggleRules((current) => [
...current,
{
id: makeId(),
target: '',
property: 'disabled',
source: currentField || '',
operator: 'empty',
value: '',
whenTrue: true,
},
])
}
>
Ekle
</Button>
</div>
<div className="flex flex-col gap-3">
{toggleRules.map((rule) => (
<div
key={rule.id}
className="rounded border border-gray-100 dark:border-gray-700 p-3"
>
<div className="mb-3 rounded bg-gray-50 px-2 py-1.5 text-xs text-gray-600 dark:bg-gray-900 dark:text-gray-300">
Eğer{' '}
<span className="font-semibold">
{fieldLabel(rule.source, 'kaynak field')}
</span>{' '}
<span className="font-semibold">{rule.operator}</span>{' '}
{rule.operator === 'equals' || rule.operator === 'notEquals' ? (
<span className="font-semibold">{rule.value || 'değer'}</span>
) : null}{' '}
ise{' '}
<span className="font-semibold">
{fieldLabel(rule.target, 'hedef field')}
</span>{' '}
<span className="font-semibold">{rule.property}</span> ={' '}
<span className="font-semibold">{String(rule.whenTrue)}</span> olur.
</div>
<div className="grid grid-cols-12 gap-2">
<div className="col-span-12 md:col-span-4">
{renderLabeledFieldSelect(
'O zaman: hedef field',
rule.target,
(nextValue) =>
setToggleRules((current) =>
current.map((item) =>
item.id === rule.id ? { ...item, target: nextValue } : item,
),
),
)}
</div>
<select
className={`${baseInputClass} col-span-12 self-end md:col-span-3`}
value={rule.property}
title="Hedef alanın hangi özelliği değişecek?"
onChange={(event) =>
setToggleRules((current) =>
current.map((item) =>
item.id === rule.id
? {
...item,
property: event.target.value as ToggleRule['property'],
}
: item,
),
)
}
>
<option value="disabled">disabled</option>
<option value="readOnly">readOnly</option>
<option value="visible">visible</option>
</select>
<div className="col-span-12 md:col-span-4">
{renderLabeledFieldSelect('Eğer: kaynak field', rule.source, (nextValue) =>
setToggleRules((current) =>
current.map((item) =>
item.id === rule.id ? { ...item, source: nextValue } : item,
),
),
)}
</div>
<Button
type="button"
shape="circle"
variant="plain"
icon={<FaTrash />}
onClick={() =>
setToggleRules((current) => current.filter((item) => item.id !== rule.id))
}
/>
</div>
<div className="grid grid-cols-12 gap-2 mt-2">
<select
className={`${baseInputClass} col-span-4`}
value={rule.operator}
title="Kaynak field için koşul operatörü."
onChange={(event) =>
setToggleRules((current) =>
current.map((item) =>
item.id === rule.id
? {
...item,
operator: event.target.value as ToggleRule['operator'],
}
: item,
),
)
}
>
<option value="empty">empty</option>
<option value="notEmpty">notEmpty</option>
<option value="equals">equals</option>
<option value="notEquals">notEquals</option>
</select>
<input
className={`${baseInputClass} col-span-12 md:col-span-4`}
value={rule.value}
disabled={rule.operator === 'empty' || rule.operator === 'notEmpty'}
onChange={(event) =>
setToggleRules((current) =>
current.map((item) =>
item.id === rule.id ? { ...item, value: event.target.value } : item,
),
)
}
placeholder="Koşul değeri"
title="Eşitse/Eşit değilse durumunda karşılaştırılacak değer."
/>
<select
className={`${baseInputClass} col-span-12 md:col-span-4`}
value={String(rule.whenTrue)}
title="Koşul doğruysa hedef property hangi değeri alsın?"
onChange={(event) =>
setToggleRules((current) =>
current.map((item) =>
item.id === rule.id
? { ...item, whenTrue: event.target.value === 'true' }
: item,
),
)
}
>
<option value="true">true</option>
<option value="false">false</option>
</select>
</div>
</div>
))}
</div>
</div>
<div className="rounded border border-gray-200 dark:border-gray-700 p-3">
<div
className="mb-3 text-sm font-semibold"
className="mb-2 text-sm font-semibold"
title="Start/End tarih alanlarından gün farkını hesaplayıp hedef field'a yazar. Örnek: StartDate + EndDate -> TotalDays."
>
4. Tarih Farkı
</div>
<div className="flex items-center gap-2 mb-2">
<Button
type="button"
size="sm"
onClick={fillDateDefaults}
title="Eski kullanım: StartDate ve EndDate değişince TotalDays alanını gün farkı olarak hesapla."
>
StartDate / EndDate / TotalDays
</Button>
3. Tarih ve Saat Farkı
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
{renderLabeledFieldSelect('Başlangıç tarihi', daysStartField, setDaysStartField)}
{renderLabeledFieldSelect('Bitiş tarihi', daysEndField, setDaysEndField)}
{renderLabeledFieldSelect('Sonuç alanı', daysTargetField, setDaysTargetField)}
</div>
</div>
<div className="rounded border border-gray-200 dark:border-gray-700 p-3">
<div className="flex items-center justify-between gap-2 mb-3">
<div>
<div
className="text-sm font-semibold"
title="Başlangıç ve bitiş saatinden saat farkını hesaplar. Bitiş başlangıçtan küçükse gece devri kabul edip 24 saat ekler."
>
5. Saat Farkı
</div>
</div>
<label className="flex items-center gap-2 rounded border border-gray-100 px-2 py-1.5 text-sm dark:border-gray-700">
<input
type="checkbox"
checked={timeDiffEnabled}
onChange={(event) => {
setTimeDiffEnabled(event.target.checked)
fillTimeDefaults()
}}
/>
<span title="Eski kullanım: StartTime ve EndTime farkını hesapla; sonuç negatifse 24 saat ekleyip TotalHours alanına yaz.">
Aktif
</span>
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 pt-2">
{renderLabeledFieldSelect('Başlangıç saati', timeStartField, setTimeStartField)}
{renderLabeledFieldSelect('Bitiş saati', timeEndField, setTimeEndField)}
{renderLabeledFieldSelect('Sonuç alanı', timeTargetField, setTimeTargetField)}
@ -1439,24 +1020,10 @@ function EditorScriptBuilderDialog({
className="mb-3 text-sm font-semibold"
title="Quantity ve UnitPrice ile TotalAmount hesaplar. Seçili kayıttan hesapla lookup kaydından değerleri alır; satır toplamını yenile mevcut form değerlerinden hesaplar."
>
6. Tutar Hesaplamaları
5. Tutar Hesaplamaları
</div>
<div className="grid grid-cols-1 gap-2 mb-3 md:grid-cols-2">
<label className="flex items-start gap-2 rounded border border-gray-100 dark:border-gray-700 px-2 py-2 text-sm">
<input
className="mt-1"
type="checkbox"
checked={selectedItemAmountEnabled}
onChange={(event) => {
setSelectedItemAmountEnabled(event.target.checked)
fillAmountDefaults()
}}
/>
<span title="Seçilen lookup/grid kaydından miktar, birim fiyat ve varsa birim bilgisini alır; toplamı bu değerlerle hesaplar.">
Seçili kayıttan miktar/fiyat al
</span>
</label>
<label className="flex items-start gap-2 rounded border border-gray-100 dark:border-gray-700 px-2 py-2 text-sm">
<label className="flex items-start gap-2 text-sm">
<input
className="mt-1"
type="checkbox"
@ -1490,9 +1057,9 @@ function EditorScriptBuilderDialog({
<div className="rounded border border-gray-200 dark:border-gray-700 p-3">
<div
className="mb-3 text-sm font-semibold"
title="Hâlâ karşılanmayan özel bir durum varsa burayı kullan. Servis çağrısı tek satırlık global servis çağrıları için, özel script ileri seviye serbest kod içindir."
title="Tek satırlık global servis çağrıları için kullan. Daha detaylı kodu sağdaki TypeScript script editor alanında düzenleyebilirsin."
>
7. Servis Çağrısı ve Özel Script
6. Servis Çağrısı
</div>
<input
className={baseInputClass}
@ -1501,27 +1068,39 @@ function EditorScriptBuilderDialog({
placeholder="UiEvalService.ApiGenerateBackgroundWorkers();"
title="Global servis çağrısı. Örnek: UiEvalService.ApiGenerateBackgroundWorkers();"
/>
<textarea
className="w-full min-h-[110px] mt-2 px-2 py-2 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm font-mono text-gray-700 dark:text-gray-100 focus:outline-none focus:border-indigo-400"
value={customScript}
onChange={(event) => setCustomScript(event.target.value)}
placeholder="İleri seviye özel script"
title="İleri seviye serbest JavaScript. Mümkünse önce Koşullu Aksiyon kullan."
/>
</div>
</section>
<section className="rounded border border-gray-200 dark:border-gray-700 p-3 flex flex-col min-h-[520px]">
<div
className="flex items-center gap-2 text-sm font-semibold mb-3"
title="Boş editorScript ve seçim yoksa preview boş kalır. Seçim yaptıkça editorScript alanına yazılacak kod burada oluşur."
className="flex items-center justify-between gap-2 text-sm font-semibold mb-3"
title="Seçim yaptıkça editorScript alanına yazılacak kod burada oluşur. Oluşan TypeScript kodunu kaydetmeden önce düzenleyebilirsin."
>
<span className="flex items-center gap-2">
<FaCode />
Script Önizleme
</span>
<span className="rounded bg-gray-100 px-2 py-1 text-[11px] font-medium text-gray-500 dark:bg-gray-800 dark:text-gray-300">
TypeScript
</span>
</div>
<div className="flex-1 min-h-[430px] overflow-hidden rounded border border-gray-200 dark:border-gray-700">
<Editor
height="100%"
language="typescript"
theme="vs-dark"
value={scriptEditorValue}
onChange={(nextValue) => setScriptEditorValue(nextValue || '')}
options={{
automaticLayout: true,
fontSize: 12,
minimap: { enabled: false },
scrollBeyondLastLine: false,
tabSize: 2,
wordWrap: 'on',
}}
/>
</div>
<pre className="flex-1 overflow-auto rounded bg-gray-50 dark:bg-gray-900 p-3 text-xs whitespace-pre-wrap">
{preview || 'Seçim yaptıkça script burada oluşacak.'}
</pre>
</section>
</div>
</Dialog.Body>
@ -1535,7 +1114,7 @@ function EditorScriptBuilderDialog({
variant="solid"
icon={<FaCheck />}
onClick={() => {
onApply(preview)
onApply(scriptEditorValue)
onClose()
}}
>

View file

@ -220,12 +220,7 @@ function JsonRowOpDialogEditForm({
invalid={errors.itemType && touched.itemType}
errorMessage={errors.itemType}
>
<Field
type="text"
autoComplete="off"
name="itemType"
placeholder="Item Type"
>
<Field type="text" autoComplete="off" name="itemType" placeholder="Item Type">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
@ -305,6 +300,16 @@ function JsonRowOpDialogEditForm({
<pre>
{`• (() => { const d=v=>!v?null:(v instanceof Date?v:new Date(v));const nf={...formData,[editor.dataField]:e?.value};const s=d(nf.StartDate),t=d(nf.EndDate);setFormData({...formData,TotalDays: s&&t?Math.max(0,Math.floor((Date.UTC(t.getFullYear(),t.getMonth(),t.getDate())-Date.UTC(s.getFullYear(),s.getMonth(),s.getDate()))/(24*60*60*1000))+1):null});})(); `}
</pre>
<pre>
{
"• (() => { const p=e.component.option('selectedItem')||{}; const q=Math.round((parseFloat(p.Quantity)||0)*100); const u=Math.round((parseFloat(p.UnitPrice)||0)*100); setFormData({ ...formData, Quantity:q/100, UnitPrice:u/100, UomId:p.UomId, TotalAmount:Math.round((q*u)/100)/100 }); })();"
}
</pre>
<pre>
{
'• (() => { const n={...formData,[e.dataField]:e.value}; const q=Math.round((parseFloat(n.Quantity)||0)*100); const u=Math.round((parseFloat(n.UnitPrice)||0)*100); setFormData({ ...n, TotalAmount:Math.round((q*u)/100)/100 }); })();'
}
</pre>
</div>
</div>
@ -531,9 +536,7 @@ function JsonRowOpDialogEditForm({
/>
</FormItem>
<FormItem
label={translate(
'::ListForms.ListFormEdit.TabColumns',
)}
label={translate('::ListForms.ListFormEdit.TabColumns')}
>
<Field
type="text"
@ -775,10 +778,7 @@ function JsonRowOpDialogEditForm({
</div>
</div>
<div className="w-1/12 ml-2 align-middle text-center">
<Field
name={`items.${index}.isRequired`}
component={Checkbox}
/>
<Field name={`items.${index}.isRequired`} component={Checkbox} />
</div>
<div className="w-1/12 ml-2">
<Field

View file

@ -1089,6 +1089,16 @@ const WizardStep3 = ({
<pre>
{`• (() => { const d=v=>!v?null:(v instanceof Date?v:new Date(v));const nf={...formData,[editor.dataField]:e?.value};const s=d(nf.StartDate),t=d(nf.EndDate);setFormData({...formData,TotalDays: s&&t?Math.max(0,Math.floor((Date.UTC(t.getFullYear(),t.getMonth(),t.getDate())-Date.UTC(s.getFullYear(),s.getMonth(),s.getDate()))/(24*60*60*1000))+1):null});})(); `}
</pre>
<pre>
{
"• (() => { const p=e.component.option('selectedItem')||{}; const q=Math.round((parseFloat(p.Quantity)||0)*100); const u=Math.round((parseFloat(p.UnitPrice)||0)*100); setFormData({ ...formData, Quantity:q/100, UnitPrice:u/100, UomId:p.UomId, TotalAmount:Math.round((q*u)/100)/100 }); })();"
}
</pre>
<pre>
{
'• (() => { const n={...formData,[e.dataField]:e.value}; const q=Math.round((parseFloat(n.Quantity)||0)*100); const u=Math.round((parseFloat(n.UnitPrice)||0)*100); setFormData({ ...n, TotalAmount:Math.round((q*u)/100)/100 }); })();'
}
</pre>
</div>
</div>

View file

@ -115,6 +115,7 @@ const Grid = (props: GridProps) => {
const gridRef = useRef<DataGridRef>()
const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null)
const editingFormDataRef = useRef<Record<string, any>>({})
// Edit popup state kaydetmeyi engellemek için flag
const isEditingRef = useRef(false)
@ -347,6 +348,7 @@ const Grid = (props: GridProps) => {
const onInitNewRow = useCallback(
(e: any) => {
if (!gridDto?.columnFormats) {
editingFormDataRef.current = { ...(e.data || {}) }
return
}
@ -436,6 +438,8 @@ const Grid = (props: GridProps) => {
}
}
}
editingFormDataRef.current = { ...(e.data || {}) }
})()
},
[gridDto, searchParams, extraFilters, getNextSequenceValue],
@ -486,6 +490,7 @@ const Grid = (props: GridProps) => {
const onEditingStart = useCallback(
(e: DataGridTypes.EditingStartEvent<any, any>) => {
isEditingRef.current = true
editingFormDataRef.current = { ...(e.data || {}) }
setMode('edit')
setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false)
const columns = e.component.option('columns') as GridColumnData[]
@ -1371,11 +1376,31 @@ const Grid = (props: GridProps) => {
colCount: 1,
onFieldDataChanged: (e) => {
if (e.dataField) {
const previousValue = editingFormDataRef.current?.[e.dataField]
editingFormDataRef.current = {
...(e.component?.option?.('formData') || {}),
}
const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => group.items || [])
.find((i) => i.dataField === e.dataField)
if (formItem?.editorScript) {
try {
const grid = gridRef.current?.instance()
const rowKey = grid?.option('editing.editRowKey')
const rowIndex =
rowKey !== undefined ? grid?.getRowIndexByKey(rowKey) : -1
const formData = e.component?.option?.('formData') || {}
const setFormData = (newData: any) => {
e.component?.option?.('formData', newData)
editingFormDataRef.current = { ...newData }
if (grid && rowIndex !== undefined && rowIndex >= 0) {
Object.keys(newData).forEach((field) => {
grid.cellValue(rowIndex, field, newData[field])
})
}
}
eval(formItem.editorScript)
} catch (err) {
console.error('Script exec error', err)

View file

@ -102,6 +102,7 @@ const Tree = (props: TreeProps) => {
const gridRef = useRef<TreeListRef>()
const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null)
const editingFormDataRef = useRef<Record<string, any>>({})
// Edit popup state kaydetmeyi engellemek için flag
const isEditingRef = useRef(false)
@ -381,6 +382,7 @@ const Tree = (props: TreeProps) => {
function onInitNewRow(e: any) {
if (!gridDto?.columnFormats) {
editingFormDataRef.current = { ...(e.data || {}) }
return
}
@ -444,6 +446,8 @@ const Tree = (props: TreeProps) => {
}
}
}
editingFormDataRef.current = { ...(e.data || {}) }
}
}
@ -483,6 +487,7 @@ const Tree = (props: TreeProps) => {
const onEditingStart = useCallback(
(e: TreeListTypes.EditingStartEvent) => {
isEditingRef.current = true
editingFormDataRef.current = { ...(e.data || {}) }
setMode('edit')
setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false)
const columns = e.component.option('columns') as GridColumnData[]
@ -1029,11 +1034,31 @@ const Tree = (props: TreeProps) => {
colCount: 1,
onFieldDataChanged: (e) => {
if (e.dataField) {
const previousValue = editingFormDataRef.current?.[e.dataField]
editingFormDataRef.current = {
...(e.component?.option?.('formData') || {}),
}
const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => group.items || [])
.find((i) => i.dataField === e.dataField)
if (formItem?.editorScript) {
try {
const grid = gridRef.current?.instance()
const rowKey = grid?.option('editing.editRowKey')
const rowIndex =
rowKey !== undefined ? grid?.getRowIndexByKey(rowKey) : -1
const formData = e.component?.option?.('formData') || {}
const setFormData = (newData: any) => {
e.component?.option?.('formData', newData)
editingFormDataRef.current = { ...newData }
if (grid && rowIndex !== undefined && rowIndex >= 0) {
Object.keys(newData).forEach((field) => {
grid.cellValue(rowIndex, field, newData[field])
})
}
}
eval(formItem.editorScript)
} catch (err) {
console.error('Script exec error', err)