Grid ve Form komponentlerine Script özelliği

FormView, FormNew, Grid Popup için Script özelliği eklendi. Ayrıca itemlara buton eklenebiliyor.
Sadece textbox olan inputlara ekleniyor. Diğer komponenler için render özelliği kullanılması gerekiyor.
This commit is contained in:
Sedat ÖZTÜRK 2025-09-24 20:46:03 +03:00
parent ea5dbe91f5
commit 6e3f58ce9d
9 changed files with 334 additions and 90 deletions

View file

@ -46,4 +46,5 @@ public class EditingFormItemDto
public bool IsRequired { get; set; }
public GridBoxOptionsDto GridBoxOptions { get; set; }
public TagBoxOptionsDto TagBoxOptions { get; set; }
public string Script { get; set; }
}

View file

@ -14124,6 +14124,12 @@
"en": "Accept CustomValue",
"tr": "Özel Değeri Kabul Et"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.HelperOpen",
"en": "Helper Codes",
"tr": "Yardımcı Kodlar"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormFieldEdit.ShowClearButton",

View file

@ -404,6 +404,7 @@ export interface EditingFormItemDto {
isRequired?: boolean
gridBoxOptions?: GridBoxOptionsDto
tagBoxOptions?: TagBoxOptionsDto
script?: string
}
export interface GridEditingPopupDto {

View file

@ -77,6 +77,7 @@ function JsonRowOpDialogEditForm({
const [fieldList, setFieldList] = useState<SelectBoxOption[]>([])
const [isOpenOptionsDialog, setIsOpenOptionsDialog] = useState<Record<number, boolean>>({})
const [isHelperOpen, setIsHelperOpen] = useState(false)
const getFields = async () => {
if (!listFormCode) {
@ -138,7 +139,7 @@ function JsonRowOpDialogEditForm({
return (
<Dialog
id="editFormOperation"
width={1000}
width={1200}
isOpen={isOpen}
preventScroll={true}
onClose={handleClose}
@ -196,80 +197,131 @@ function JsonRowOpDialogEditForm({
{({ touched, errors, values, isSubmitting }) => (
<Form>
<FormContainer size="sm">
<div className="max-h-96 overflow-y-auto p-2">
<FormItem
label="Order"
invalid={errors.order && touched.order}
errorMessage={errors.order}
<div className="max-h-[90vh] overflow-y-auto p-2">
<div className="grid grid-cols-5 gap-4 w-full">
<FormItem
label="Order"
invalid={errors.order && touched.order}
errorMessage={errors.order}
>
<Field
type="number"
autoComplete="off"
name="order"
placeholder="Order"
component={Input}
/>
</FormItem>
<FormItem
label="Item Type"
invalid={errors.itemType && touched.itemType}
errorMessage={errors.itemType}
>
<Field
type="text"
autoComplete="off"
name="itemType"
placeholder="Item Type"
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={itemTypeOptions}
value={itemTypeOptions?.filter(
(option) => option.value === values.itemType,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Caption"
invalid={errors.caption && touched.caption}
errorMessage={errors.caption}
>
<Field
autoFocus={true}
type="text"
autoComplete="off"
name="caption"
placeholder="Caption"
component={Input}
/>
</FormItem>
<FormItem
label="Column Count"
invalid={errors.colCount && touched.colCount}
errorMessage={errors.colCount}
>
<Field
type="number"
autoComplete="off"
name="colCount"
placeholder="Column Count"
component={Input}
/>
</FormItem>
<FormItem
label="Column Span"
invalid={errors.colSpan && touched.colSpan}
errorMessage={errors.colSpan}
>
<Field
type="number"
autoComplete="off"
name="colSpan"
placeholder="Column Span"
component={Input}
/>
</FormItem>
</div>
<Dialog
isOpen={isHelperOpen}
onClose={() => setIsHelperOpen(false)}
onRequestClose={() => setIsHelperOpen(false)}
preventScroll={true}
width={1000}
>
<Field
type="number"
autoComplete="off"
name="order"
placeholder="Order"
component={Input}
/>
</FormItem>
<FormItem
label="Item Type"
invalid={errors.itemType && touched.itemType}
errorMessage={errors.itemType}
>
<Field type="text" autoComplete="off" name="itemType" placeholder="Item Type">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={itemTypeOptions}
value={itemTypeOptions?.filter(
(option) => option.value === values.itemType,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label="Caption"
invalid={errors.caption && touched.caption}
errorMessage={errors.caption}
>
<Field
autoFocus={true}
type="text"
autoComplete="off"
name="caption"
placeholder="Caption"
component={Input}
/>
</FormItem>
<FormItem
label="Column Count"
invalid={errors.colCount && touched.colCount}
errorMessage={errors.colCount}
>
<Field
type="number"
autoComplete="off"
name="colCount"
placeholder="Column Count"
component={Input}
/>
</FormItem>
<FormItem
label="Column Span"
invalid={errors.colSpan && touched.colSpan}
errorMessage={errors.colSpan}
>
<Field
type="number"
autoComplete="off"
name="colSpan"
placeholder="Column Span"
component={Input}
/>
</FormItem>
<h5 className="mb-4">Helper</h5>
<div className="space-y-4 p-4 text-sm font-mono overflow-y-auto max-h-[100vh]">
<div>
<div className="font-bold"> Editor Script</div>
<div className="ml-5 gap-2">
<div>{'• setFormData({...formData, Path: e.value});'}</div>
<div>{'• UiEvalService.ApiGenerateBackgroundWorkers();'}</div>
<div>
{
"• setFormData({ ...formData, Path: (v => v === '1' ? '1-deneme' : v === '0' ? '0-deneme' : '')(e.value) })"
}
</div>
</div>
</div>
<div>
<div className="font-bold"> Editor Options</div>
<pre className="ml-5 whitespace-pre-wrap text-xs">
{`• {
"buttons": [
{
"name": "custom",
"location": "after",
"options": {
"icon": "plus",
"onClick": "function(e) { alert('Value: ' + e.formData[e.fieldName]); }"
}
}
]
}`}
</pre>
</div>
</div>
</Dialog>
<FormItem
label="Items"
invalid={errors.items && touched.items}
@ -281,9 +333,10 @@ function JsonRowOpDialogEditForm({
<div>
<div className="flex m-1 font-bold text-center">
<div className="w-1/12">Order</div>
<div className="w-3/12">Data Field</div>
<div className="w-2/12">Data Field</div>
<div className="w-2/12">Editor Type</div>
<div className="w-3/12">Editor Options</div>
<div className="w-2/12">Editor Options</div>
<div className="w-2/12">Editor Script</div>
<div className="w-1/12">Is Required</div>
<div className="w-1/12">Colspan</div>
<div className="w-1/12"></div>
@ -300,7 +353,7 @@ function JsonRowOpDialogEditForm({
placeholder="Order"
/>
</div>
<div className="w-3/12 ml-2">
<div className="w-2/12 ml-2">
<Field
type="text"
autoFocus={true}
@ -332,7 +385,7 @@ function JsonRowOpDialogEditForm({
)}
</Field>
</div>
<div className="w-3/12 ml-2 flex gap-2">
<div className="w-2/12 ml-2 flex gap-2">
<Field
type="text"
autoComplete="off"
@ -674,6 +727,15 @@ function JsonRowOpDialogEditForm({
placeholder="Editor Options"
/>
</div>
<div className="w-2/12 ml-2">
<Field
type="text"
autoComplete="off"
name={`items.${index}.script`}
component={Input}
placeholder="Script"
/>
</div>
<div className="w-1/12 ml-2 align-middle text-center">
<Field
name={`items.${index}.isRequired`}
@ -734,6 +796,14 @@ function JsonRowOpDialogEditForm({
</FormItem>
</div>
<div className="text-right mt-4">
<Button
type="button"
variant="solid"
className="text-white bg-red-600 hover:bg-red-700 ltr:mr-2 rtl:ml-2"
onClick={() => setIsHelperOpen(true)}
>
{translate('::ListForms.ListFormFieldEdit.HelperOpen')}
</Button>
<Button className="ltr:mr-2 rtl:ml-2" variant="plain" onClick={handleClose}>
Cancel
</Button>

View file

@ -6,7 +6,7 @@ import {
SimpleItem as SimpleItemDx,
} from 'devextreme-react/form'
import { FieldDataChangedEvent, GroupItem } from 'devextreme/ui/form'
import { Dispatch, RefObject } from 'react'
import { Dispatch, RefObject, useEffect, useRef } from 'react'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { RowMode, SimpleItemWithColData } from './types'
@ -23,19 +23,41 @@ const FormDevExpress = (props: {
}) => {
const { listFormCode, isSubForm, mode, refForm, formData, formItems, setFormData } = props
const formDataRef = useRef(formData)
useEffect(() => {
formDataRef.current = formData
}, [formData])
return (
<form className={`${DX_CLASSNAMES} ${!isSubForm ? 'px-2' : ''} pb-2`}>
<FormDx
ref={refForm}
formData={formData}
onFieldDataChanged={(e: FieldDataChangedEvent) => {
onFieldDataChanged={async (e: FieldDataChangedEvent) => {
setFormData({ ...formData, [e.dataField!]: e.value })
//Dinamik script
const changeItem = formItems
.flatMap((group) => (group.items as SimpleItemWithColData[]) || [])
.find((i: SimpleItemWithColData) => i.dataField === e.dataField)
if (changeItem?.script) {
try {
//setFormData({...formData, Path: e.value});
//UiEvalService.ApiGenerateBackgroundWorkers();
//setFormData({ ...formData, Path: (v => v === '1' ? '1-deneme' : v === '0' ? '0-deneme' : '')(e.value) })
eval(changeItem.script)
} catch (err) {
console.error('Script execution failed for', changeItem.name, err)
}
}
}}
onContentReady={(e) => {
const firstItem = formItems?.flatMap((g) => g.items || []).find((it) => !!it.name)
const groupItems = e.component.option('items') as any[]
const firstItem = groupItems?.[0]?.items?.[0]
if (firstItem?.name) {
const editor = e.component.getEditor(firstItem.name as string)
if (firstItem?.dataField) {
const editor = e.component.getEditor(firstItem.dataField)
editor?.focus()
}
}}
@ -43,7 +65,6 @@ const FormDevExpress = (props: {
{formItems.map((formGroupItem, i) => {
return (
<GroupItemDx
key={'formGroupItem-' + i}
colCount={formGroupItem.colCount}
colSpan={formGroupItem.colSpan}
@ -97,12 +118,26 @@ const FormDevExpress = (props: {
</SimpleItemDx>
) : (
<SimpleItemDx
cssClass='font-semibold'
cssClass="font-semibold"
key={'formItem-' + i}
{...formItem}
editorOptions={{
...formItem.editorOptions,
...(mode === 'view' ? { readOnly: true } : { autoFocus: i === 1 }),
buttons: (formItem.editorOptions?.buttons || []).map((btn: any) => {
if (btn?.options?.onClick && typeof btn.options.onClick === 'string') {
const origClick = eval(`(${btn.options.onClick})`)
btn.options.onClick = (e: any) => {
origClick({
...e,
formData: formDataRef.current, // 🔥 her zaman güncel state
fieldName: formItem.dataField,
mode, // "new" | "edit" | "view"
})
}
}
return btn
}),
}}
/>
)

View file

@ -1,6 +1,11 @@
import { FormItemComponent, SimpleItem } from 'devextreme/ui/form'
import { Overwrite } from '../../utils/types'
import { ColumnFormatDto, GridBoxOptionsDto, PlatformEditorTypes, TagBoxOptionsDto } from '../../proxy/form/models'
import {
ColumnFormatDto,
GridBoxOptionsDto,
PlatformEditorTypes,
TagBoxOptionsDto,
} from '../../proxy/form/models'
import { Meta } from '@/@types/routes'
export type EditorType2 = FormItemComponent | PlatformEditorTypes.dxGridBox
@ -15,6 +20,7 @@ export type SimpleItemWithColData = Overwrite<
canUpdate: boolean
canExport: boolean
editorType2: EditorType2
script?: string
}
>
export type RowMode = 'view' | 'edit' | 'new'

View file

@ -260,6 +260,7 @@ const useGridData = (props: {
colData,
tagBoxOptions: i.tagBoxOptions,
gridBoxOptions: i.gridBoxOptions,
script: i.script,
}
if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) }

View file

@ -20,7 +20,6 @@ import { Container, Loading } from '@/components/shared'
import WidgetGroup from '@/components/common/WidgetGroup'
import { GridExtraFilterState } from './Utils'
import { useStoreActions, useStoreState } from '@/store/store'
import { FaTrashCan } from 'react-icons/fa6'
const CardItem = ({
isSubForm,
@ -95,6 +94,7 @@ const CardItem = ({
colData,
tagBoxOptions: i.tagBoxOptions,
gridBoxOptions: i.gridBoxOptions,
script: i.script,
}
if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) }
@ -383,7 +383,7 @@ const Card = (props: CardProps) => {
}}
className="p-1 pl-6 pr-2 border border-1 outline-none text-xs text-gray-700 dark:text-gray-200 placeholder-gray-400 rounded"
/>
<Button
size="xs"
variant={layoutCount === 1 ? 'solid' : 'default'}

View file

@ -102,6 +102,10 @@ const Grid = (props: GridProps) => {
import('devextreme/pdf_exporter')
}
type EditorOptionsWithButtons = {
buttons?: any[]
} & Record<string, any>
const defaultSearchParams = useRef<string | null>(null)
useEffect(() => {
@ -506,7 +510,6 @@ const Grid = (props: GridProps) => {
searchParams?.delete('filter')
}
gridRef.current?.instance.refresh()
}, [extraFilters])
@ -664,6 +667,81 @@ const Grid = (props: GridProps) => {
onRowRemoved={() => {
props.refreshData?.()
}}
onEditorPreparing={(editor) => {
if (editor.parentType === 'dataRow' && editor.dataField) {
const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => group.items || [])
.find((i) => i.dataField === editor.dataField)
if (formItem?.script) {
const prevHandler = editor.editorOptions.onValueChanged // varsa önceki handler'ı sakla
editor.editorOptions.onValueChanged = (e: any) => {
// yeni handler
if (prevHandler) prevHandler(e)
try {
const grid = editor.component
const rowKey = grid.option('editing.editRowKey')
const rowIndex = grid.getRowIndexByKey(rowKey)
// formData → aktif satırın datası
const formData =
grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {}
// setFormData → komple yeni obje vererek güncelleme
const setFormData = (newData: any) => {
if (rowIndex >= 0) {
Object.keys(newData).forEach((field) => {
grid.cellValue(rowIndex, field, newData[field])
})
}
}
eval(formItem.script!)
//setFormData({ ...formData, Path: e.value, Authority: e.value })
// editor.component.cellValue(
// editor.component.getRowIndexByKey(
// editor.component.option('editing.editRowKey'),
// ),
// 'Path',
// e.value,
// )
} catch (err) {
console.error('Script exec error', formItem.dataField, err)
}
}
}
// 🔥 Butonların onClick eventlerini wrap et
if (editor.editorOptions?.buttons) {
editor.editorOptions.buttons = editor.editorOptions.buttons.map(
(btn: any) => {
if (btn?.options?.onClick && typeof btn.options.onClick === 'function') {
const origClick = btn.options.onClick
btn.options.onClick = (e: any) => {
const grid = editor.component
const rowKey = grid.option('editing.editRowKey')
const rowIndex = grid.getRowIndexByKey(rowKey)
const formData =
grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {}
origClick({
...e,
formData,
fieldName: editor.dataField,
rowKey,
rowIndex,
})
}
}
return btn
},
)
}
}
}}
>
<Export
enabled={true}
@ -693,6 +771,23 @@ const Grid = (props: GridProps) => {
fullScreen: gridDto.gridOptions.editingOptionDto?.popup?.fullScreen,
}}
form={{
onFieldDataChanged: (e) => {
if (e.dataField) {
const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => group.items || [])
.find((i) => i.dataField === e.dataField)
if (formItem?.script) {
try {
//setFormData({...formData, Path: e.value});
//UiEvalService.ApiGenerateBackgroundWorkers();
//setFormData({ ...formData, Path: (v => v === '1' ? '1-deneme' : v === '0' ? '0-deneme' : '')(e.value) })
eval(formItem.script)
} catch (err) {
console.error('Script exec error', err)
}
}
}
},
items:
gridDto.gridOptions.editingFormDto?.length > 0
? gridDto.gridOptions.editingFormDto
@ -710,10 +805,38 @@ const Grid = (props: GridProps) => {
return a.order >= b.order ? 1 : -1
})
.map((i: EditingFormItemDto) => {
let editorOptions = {}
let editorOptions: EditorOptionsWithButtons = {}
try {
editorOptions = i.editorOptions && JSON.parse(i.editorOptions)
if (editorOptions?.buttons) {
/*
{
"buttons": [
{
"name": "custom",
"location": "after",
"options": {
"icon": "plus",
"onClick": "function(e) { alert('Button clicked for ' + formData[i.dataField]); }"
}
}
]
}
*/
editorOptions.buttons = (editorOptions?.buttons || []).map(
(btn: any) => {
if (
btn?.options?.onClick &&
typeof btn.options.onClick === 'string'
) {
btn.options.onClick = eval(`(${btn.options.onClick})`)
}
return btn
},
)
}
// Eğer default value varsa, bu editörü readonly yapıyoruz
const rawFilter = searchParams?.get('filter')
if (rawFilter) {
@ -780,6 +903,7 @@ const Grid = (props: GridProps) => {
colSpan: i.colSpan,
isRequired: i.isRequired,
editorOptions,
script: i.script,
}
if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) }