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 bool IsRequired { get; set; }
public GridBoxOptionsDto GridBoxOptions { get; set; } public GridBoxOptionsDto GridBoxOptions { get; set; }
public TagBoxOptionsDto TagBoxOptions { get; set; } public TagBoxOptionsDto TagBoxOptions { get; set; }
public string Script { get; set; }
} }

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import {
SimpleItem as SimpleItemDx, SimpleItem as SimpleItemDx,
} from 'devextreme-react/form' } from 'devextreme-react/form'
import { FieldDataChangedEvent, GroupItem } from 'devextreme/ui/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 { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent' import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { RowMode, SimpleItemWithColData } from './types' import { RowMode, SimpleItemWithColData } from './types'
@ -23,19 +23,41 @@ const FormDevExpress = (props: {
}) => { }) => {
const { listFormCode, isSubForm, mode, refForm, formData, formItems, setFormData } = props const { listFormCode, isSubForm, mode, refForm, formData, formItems, setFormData } = props
const formDataRef = useRef(formData)
useEffect(() => {
formDataRef.current = formData
}, [formData])
return ( return (
<form className={`${DX_CLASSNAMES} ${!isSubForm ? 'px-2' : ''} pb-2`}> <form className={`${DX_CLASSNAMES} ${!isSubForm ? 'px-2' : ''} pb-2`}>
<FormDx <FormDx
ref={refForm} ref={refForm}
formData={formData} formData={formData}
onFieldDataChanged={(e: FieldDataChangedEvent) => { onFieldDataChanged={async (e: FieldDataChangedEvent) => {
setFormData({ ...formData, [e.dataField!]: e.value }) 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) => { 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) { if (firstItem?.dataField) {
const editor = e.component.getEditor(firstItem.name as string) const editor = e.component.getEditor(firstItem.dataField)
editor?.focus() editor?.focus()
} }
}} }}
@ -43,7 +65,6 @@ const FormDevExpress = (props: {
{formItems.map((formGroupItem, i) => { {formItems.map((formGroupItem, i) => {
return ( return (
<GroupItemDx <GroupItemDx
key={'formGroupItem-' + i} key={'formGroupItem-' + i}
colCount={formGroupItem.colCount} colCount={formGroupItem.colCount}
colSpan={formGroupItem.colSpan} colSpan={formGroupItem.colSpan}
@ -97,12 +118,26 @@ const FormDevExpress = (props: {
</SimpleItemDx> </SimpleItemDx>
) : ( ) : (
<SimpleItemDx <SimpleItemDx
cssClass='font-semibold' cssClass="font-semibold"
key={'formItem-' + i} key={'formItem-' + i}
{...formItem} {...formItem}
editorOptions={{ editorOptions={{
...formItem.editorOptions, ...formItem.editorOptions,
...(mode === 'view' ? { readOnly: true } : { autoFocus: i === 1 }), ...(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 { FormItemComponent, SimpleItem } from 'devextreme/ui/form'
import { Overwrite } from '../../utils/types' 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' import { Meta } from '@/@types/routes'
export type EditorType2 = FormItemComponent | PlatformEditorTypes.dxGridBox export type EditorType2 = FormItemComponent | PlatformEditorTypes.dxGridBox
@ -15,6 +20,7 @@ export type SimpleItemWithColData = Overwrite<
canUpdate: boolean canUpdate: boolean
canExport: boolean canExport: boolean
editorType2: EditorType2 editorType2: EditorType2
script?: string
} }
> >
export type RowMode = 'view' | 'edit' | 'new' export type RowMode = 'view' | 'edit' | 'new'

View file

@ -260,6 +260,7 @@ const useGridData = (props: {
colData, colData,
tagBoxOptions: i.tagBoxOptions, tagBoxOptions: i.tagBoxOptions,
gridBoxOptions: i.gridBoxOptions, gridBoxOptions: i.gridBoxOptions,
script: i.script,
} }
if (i.dataField.indexOf(':') >= 0) { if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) } 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 WidgetGroup from '@/components/common/WidgetGroup'
import { GridExtraFilterState } from './Utils' import { GridExtraFilterState } from './Utils'
import { useStoreActions, useStoreState } from '@/store/store' import { useStoreActions, useStoreState } from '@/store/store'
import { FaTrashCan } from 'react-icons/fa6'
const CardItem = ({ const CardItem = ({
isSubForm, isSubForm,
@ -95,6 +94,7 @@ const CardItem = ({
colData, colData,
tagBoxOptions: i.tagBoxOptions, tagBoxOptions: i.tagBoxOptions,
gridBoxOptions: i.gridBoxOptions, gridBoxOptions: i.gridBoxOptions,
script: i.script,
} }
if (i.dataField.indexOf(':') >= 0) { if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) } 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" 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 <Button
size="xs" size="xs"
variant={layoutCount === 1 ? 'solid' : 'default'} variant={layoutCount === 1 ? 'solid' : 'default'}

View file

@ -102,6 +102,10 @@ const Grid = (props: GridProps) => {
import('devextreme/pdf_exporter') import('devextreme/pdf_exporter')
} }
type EditorOptionsWithButtons = {
buttons?: any[]
} & Record<string, any>
const defaultSearchParams = useRef<string | null>(null) const defaultSearchParams = useRef<string | null>(null)
useEffect(() => { useEffect(() => {
@ -506,7 +510,6 @@ const Grid = (props: GridProps) => {
searchParams?.delete('filter') searchParams?.delete('filter')
} }
gridRef.current?.instance.refresh() gridRef.current?.instance.refresh()
}, [extraFilters]) }, [extraFilters])
@ -664,6 +667,81 @@ const Grid = (props: GridProps) => {
onRowRemoved={() => { onRowRemoved={() => {
props.refreshData?.() 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 <Export
enabled={true} enabled={true}
@ -693,6 +771,23 @@ const Grid = (props: GridProps) => {
fullScreen: gridDto.gridOptions.editingOptionDto?.popup?.fullScreen, fullScreen: gridDto.gridOptions.editingOptionDto?.popup?.fullScreen,
}} }}
form={{ 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: items:
gridDto.gridOptions.editingFormDto?.length > 0 gridDto.gridOptions.editingFormDto?.length > 0
? gridDto.gridOptions.editingFormDto ? gridDto.gridOptions.editingFormDto
@ -710,10 +805,38 @@ const Grid = (props: GridProps) => {
return a.order >= b.order ? 1 : -1 return a.order >= b.order ? 1 : -1
}) })
.map((i: EditingFormItemDto) => { .map((i: EditingFormItemDto) => {
let editorOptions = {} let editorOptions: EditorOptionsWithButtons = {}
try { try {
editorOptions = i.editorOptions && JSON.parse(i.editorOptions) 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 // Eğer default value varsa, bu editörü readonly yapıyoruz
const rawFilter = searchParams?.get('filter') const rawFilter = searchParams?.get('filter')
if (rawFilter) { if (rawFilter) {
@ -780,6 +903,7 @@ const Grid = (props: GridProps) => {
colSpan: i.colSpan, colSpan: i.colSpan,
isRequired: i.isRequired, isRequired: i.isRequired,
editorOptions, editorOptions,
script: i.script,
} }
if (i.dataField.indexOf(':') >= 0) { if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) } item.label = { text: captionize(i.dataField.split(':')[1]) }