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.
513 lines
17 KiB
TypeScript
513 lines
17 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||
import { EditingFormItemDto, GridDto, PlatformEditorTypes } from '@/proxy/form/models'
|
||
import { captionize } from 'devextreme/core/utils/inflector'
|
||
import { useListFormCustomDataSource } from '@/shared/useListFormCustomDataSource'
|
||
import { Button, Pagination, Select } from '@/components/ui'
|
||
import classNames from 'classnames'
|
||
import { FaSearch } from 'react-icons/fa'
|
||
import FormDevExpress from '../form/FormDevExpress'
|
||
import { GroupItem } from 'devextreme/ui/form'
|
||
import { Form as FormDx } from 'devextreme-react/form'
|
||
import FormButtons from '../form/FormButtons'
|
||
import { Link, useNavigate } from 'react-router-dom'
|
||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||
import CustomStore from 'devextreme/data/custom_store'
|
||
import { PermissionResults, SimpleItemWithColData } from '../form/types'
|
||
import { usePermission } from '@/utils/hooks/usePermission'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
import { useLookupDataSource } from '../form/useLookupDataSource'
|
||
import { Container, Loading } from '@/components/shared'
|
||
import WidgetGroup from '@/components/common/WidgetGroup'
|
||
import { GridExtraFilterState } from './Utils'
|
||
import { useStoreActions, useStoreState } from '@/store/store'
|
||
|
||
const CardItem = ({
|
||
isSubForm,
|
||
row,
|
||
gridDto,
|
||
listFormCode,
|
||
dataSource,
|
||
refreshData,
|
||
getCachedLookupDataSource,
|
||
}: {
|
||
isSubForm?: boolean
|
||
row: any
|
||
gridDto: GridDto
|
||
listFormCode: string
|
||
dataSource: CustomStore
|
||
refreshData: () => void
|
||
getCachedLookupDataSource: (editorOptions: any, colData: any) => any
|
||
}) => {
|
||
const [formData, setFormData] = useState(row)
|
||
const refForm = useRef<FormDx>(null)
|
||
const navigate = useNavigate()
|
||
const { checkPermission } = usePermission()
|
||
const { translate } = useLocalization()
|
||
|
||
const keyField = gridDto.gridOptions.keyFieldName
|
||
const rowId = row[keyField!]
|
||
|
||
// Form Items
|
||
const formItems: GroupItem[] = useMemo(() => {
|
||
if (!gridDto) return []
|
||
|
||
return gridDto.gridOptions.editingFormDto
|
||
?.sort((a, b) => (a.order >= b.order ? 1 : -1))
|
||
.map((e) => {
|
||
return {
|
||
itemType: e.itemType,
|
||
colCount: e.colCount,
|
||
colSpan: e.colSpan,
|
||
caption: e.caption,
|
||
items: e.items
|
||
?.sort((a, b) => (a.order >= b.order ? 1 : -1))
|
||
.map((i: EditingFormItemDto) => {
|
||
let editorOptions = {}
|
||
const colData = gridDto.columnFormats.find((x) => x.fieldName === i.dataField)
|
||
|
||
try {
|
||
editorOptions = i.editorOptions && JSON.parse(i.editorOptions)
|
||
} catch {}
|
||
|
||
const item: SimpleItemWithColData = {
|
||
canRead: colData?.canRead ?? false,
|
||
canUpdate: colData?.canUpdate ?? false,
|
||
canCreate: colData?.canCreate ?? false,
|
||
canExport: colData?.canExport ?? false,
|
||
dataField: i.dataField,
|
||
name: i.dataField,
|
||
editorType2: i.editorType2,
|
||
editorType:
|
||
i.editorType2 == PlatformEditorTypes.dxGridBox ? 'dxDropDownBox' : i.editorType2,
|
||
colSpan: i.colSpan,
|
||
isRequired: i.isRequired,
|
||
editorOptions: {
|
||
...editorOptions,
|
||
...(colData?.lookupDto?.dataSourceType
|
||
? {
|
||
dataSource: getCachedLookupDataSource(colData?.editorOptions, colData),
|
||
valueExpr: colData?.lookupDto?.valueExpr?.toLowerCase(),
|
||
displayExpr: colData?.lookupDto?.displayExpr?.toLowerCase(),
|
||
}
|
||
: {}),
|
||
},
|
||
colData,
|
||
tagBoxOptions: i.tagBoxOptions,
|
||
gridBoxOptions: i.gridBoxOptions,
|
||
script: i.script,
|
||
}
|
||
if (i.dataField.indexOf(':') >= 0) {
|
||
item.label = { text: captionize(i.dataField.split(':')[1]) }
|
||
}
|
||
item.editorOptions = {
|
||
...item.editorOptions,
|
||
readOnly: true,
|
||
}
|
||
return item
|
||
}),
|
||
} as GroupItem
|
||
})
|
||
}, [gridDto, getCachedLookupDataSource])
|
||
|
||
const permissionResults: PermissionResults = {
|
||
c:
|
||
gridDto?.gridOptions.editingOptionDto.allowAdding === true &&
|
||
checkPermission(gridDto?.gridOptions.permissionDto.c),
|
||
r: checkPermission(gridDto?.gridOptions.permissionDto.r),
|
||
u:
|
||
gridDto?.gridOptions.editingOptionDto.allowUpdating === true &&
|
||
checkPermission(gridDto?.gridOptions.permissionDto.u),
|
||
d:
|
||
gridDto?.gridOptions.editingOptionDto.allowDeleting === true &&
|
||
checkPermission(gridDto?.gridOptions.permissionDto.d),
|
||
e: checkPermission(gridDto?.gridOptions.permissionDto.e),
|
||
i: checkPermission(gridDto?.gridOptions.permissionDto.i),
|
||
}
|
||
|
||
const onActionEdit = () => {
|
||
navigate(
|
||
ROUTES_ENUM.protected.admin.formEdit
|
||
.replace(':listFormCode', listFormCode)
|
||
.replace(':id', rowId!),
|
||
)
|
||
}
|
||
|
||
const onActionNew = () => {
|
||
navigate(ROUTES_ENUM.protected.admin.formNew.replace(':listFormCode', listFormCode))
|
||
}
|
||
|
||
return (
|
||
<div className="bg-white dark:bg-neutral-800 border dark:border-neutral-700 flex flex-col">
|
||
<div
|
||
className={`flex items-center pt-2 px-2 ${isSubForm ? 'justify-end' : 'justify-between'}`}
|
||
>
|
||
{!isSubForm && <h3>{translate('::' + gridDto?.gridOptions.title)}</h3>}
|
||
{permissionResults && (
|
||
<FormButtons
|
||
isSubForm={true}
|
||
mode="view"
|
||
listFormCode={listFormCode}
|
||
id={rowId}
|
||
gridDto={gridDto}
|
||
commandColumnData={{ buttons: [] }}
|
||
dataSource={dataSource}
|
||
permissions={permissionResults}
|
||
handleSubmit={() => ({})}
|
||
refreshData={refreshData}
|
||
getSelectedRowKeys={() => [rowId]}
|
||
getSelectedRowsData={() => [row]}
|
||
getFilter={() => null}
|
||
onActionEdit={onActionEdit}
|
||
onActionNew={onActionNew}
|
||
/>
|
||
)}
|
||
</div>
|
||
<div className="flex-grow">
|
||
<FormDevExpress
|
||
mode="view"
|
||
refForm={refForm}
|
||
formData={formData}
|
||
formItems={formItems}
|
||
setFormData={setFormData}
|
||
listFormCode={listFormCode}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
interface CardProps {
|
||
listFormCode: string
|
||
searchParams?: URLSearchParams
|
||
isSubForm?: boolean
|
||
level?: number
|
||
refreshData?: () => Promise<void>
|
||
gridDto?: GridDto
|
||
}
|
||
|
||
type Option = {
|
||
value: number
|
||
label: string
|
||
}
|
||
|
||
const Card = (props: CardProps) => {
|
||
const { listFormCode, searchParams, gridDto } = props
|
||
const { createSelectDataSource } = useListFormCustomDataSource({})
|
||
const [data, setData] = useState<any[]>([])
|
||
const [totalCount, setTotalCount] = useState(0)
|
||
const [currentPage, setCurrentPage] = useState(1)
|
||
const [pageSize, setPageSize] = useState(20)
|
||
const [pageSizeOptions, setPageSizeOptions] = useState<Option[]>([])
|
||
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
|
||
const { getLookupDataSource } = useLookupDataSource({ listFormCode })
|
||
const [layoutCount, setLayoutCount] = useState(4)
|
||
const [searchText, setSearchText] = useState('')
|
||
const [prevValue, setPrevValue] = useState('')
|
||
const [loading, setLoading] = useState(false)
|
||
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
|
||
|
||
const { states } = useStoreState((state) => state.base.lists)
|
||
const { setStates } = useStoreActions((a) => a.base.lists)
|
||
|
||
// props.searchParams varsa onunla başlat
|
||
const [urlSearchParams, setUrlSearchParams] = useState<URLSearchParams>(
|
||
searchParams ? new URLSearchParams(searchParams) : new URLSearchParams(),
|
||
)
|
||
|
||
const lookupCache = useRef<Map<string, any>>(new Map())
|
||
const getCachedLookupDataSource = useCallback(
|
||
(editorOptions: any, colData: any) => {
|
||
const key = colData?.fieldName
|
||
if (!key) return null
|
||
|
||
if (!lookupCache.current.has(key)) {
|
||
lookupCache.current.set(key, getLookupDataSource(editorOptions, colData))
|
||
}
|
||
return lookupCache.current.get(key)
|
||
},
|
||
[getLookupDataSource],
|
||
)
|
||
|
||
const onPageSizeSelect = ({ value }: Option) => {
|
||
setPageSize(value)
|
||
setCurrentPage(1)
|
||
}
|
||
|
||
const onPageChange = (page: number) => {
|
||
setCurrentPage(page)
|
||
}
|
||
|
||
const onFilter = useCallback(
|
||
(value?: string) => {
|
||
const text = value !== undefined ? value.trim() : searchText.trim()
|
||
|
||
if (!gridDto?.columnFormats) return
|
||
|
||
const newParams = new URLSearchParams(urlSearchParams.toString())
|
||
|
||
if (!text) {
|
||
newParams.delete('filter')
|
||
setUrlSearchParams(newParams)
|
||
return
|
||
}
|
||
|
||
const merged = gridDto.columnFormats
|
||
.filter(
|
||
(col) =>
|
||
col.dataType === 'string' &&
|
||
col.visible &&
|
||
col.width &&
|
||
col.allowSearch &&
|
||
col.width > 0,
|
||
)
|
||
.map((col) => [col.fieldName, 'contains', text])
|
||
|
||
let filter: any = null
|
||
if (merged.length === 1) {
|
||
filter = merged[0]
|
||
} else if (merged.length > 1) {
|
||
filter = merged.reduce((acc, f, idx) => {
|
||
if (idx === 0) return f
|
||
return [acc, 'or', f]
|
||
}, null as any)
|
||
}
|
||
|
||
if (filter) {
|
||
newParams.set('filter', JSON.stringify(filter))
|
||
} else {
|
||
newParams.delete('filter')
|
||
}
|
||
|
||
setUrlSearchParams(newParams)
|
||
},
|
||
[gridDto, urlSearchParams, searchText],
|
||
)
|
||
|
||
useEffect(() => {
|
||
if (gridDto) {
|
||
const dataSource = createSelectDataSource(gridDto.gridOptions, listFormCode, urlSearchParams)
|
||
setGridDataSource(dataSource)
|
||
|
||
//listFormStates
|
||
const listFormStates = states.find((a) => a.listFormCode === listFormCode)
|
||
if (listFormStates) {
|
||
setLayoutCount(listFormStates.cardLayoutColumn || 4)
|
||
} else {
|
||
setLayoutCount(gridDto.gridOptions.layoutDto.cardLayoutColumn || 4)
|
||
}
|
||
}
|
||
}, [gridDto, listFormCode, urlSearchParams, createSelectDataSource])
|
||
|
||
const loadData = useCallback(() => {
|
||
if (!gridDataSource) return
|
||
setLoading(true)
|
||
|
||
const loadOptions = {
|
||
skip: (currentPage - 1) * pageSize,
|
||
take: pageSize,
|
||
requireTotalCount: true,
|
||
}
|
||
|
||
gridDataSource
|
||
.load(loadOptions)
|
||
.then((res: any) => {
|
||
setData(res.data)
|
||
setTotalCount(res.totalCount || 0)
|
||
setLoading(false)
|
||
})
|
||
.catch(() => {
|
||
setLoading(false)
|
||
})
|
||
}, [gridDataSource, currentPage, pageSize])
|
||
|
||
useEffect(() => {
|
||
if (gridDataSource) {
|
||
loadData()
|
||
}
|
||
}, [gridDataSource, loadData])
|
||
|
||
useEffect(() => {
|
||
if (!gridDto) return
|
||
|
||
const pagerOptions = gridDto.gridOptions.pagerOptionDto
|
||
const allowedSizes = pagerOptions?.allowedPageSizes
|
||
?.split(',')
|
||
.map((s) => Number(s.trim()))
|
||
.filter((n) => !isNaN(n) && n > 0) || [10, 20, 50, 100]
|
||
|
||
setPageSizeOptions(allowedSizes.map((size) => ({ value: size, label: `${size} page` })))
|
||
}, [gridDto, listFormCode, searchParams])
|
||
|
||
useEffect(() => {
|
||
if (data.length > 0) {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||
|
||
if (data.length < 6) {
|
||
setLayoutCount(data.length)
|
||
}
|
||
}
|
||
}, [data])
|
||
|
||
if (!gridDto) return null
|
||
|
||
return (
|
||
<>
|
||
<WidgetGroup widgetGroups={gridDto.widgets || []} />
|
||
|
||
<Container>
|
||
<div className="p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 ">
|
||
<div className="flex justify-end items-center">
|
||
<div className="relative py-1 flex gap-1 border-b-1">
|
||
<FaSearch className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-sm" />
|
||
<input
|
||
type="text"
|
||
placeholder="Search..."
|
||
value={searchText}
|
||
onChange={(e) => setSearchText(e.target.value)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') {
|
||
onFilter(e.currentTarget.value)
|
||
setPrevValue(e.currentTarget.value.trim()) // Enter ile tetiklenirse güncelle
|
||
}
|
||
}}
|
||
onBlur={(e) => {
|
||
const newValue = e.currentTarget.value.trim()
|
||
|
||
// 1. Değer değişmemişse => hiçbir şey yapma
|
||
if (newValue === prevValue) return
|
||
|
||
// 2. Yeni değer boş, ama eskiden değer vardı => filtre temizle
|
||
// 3. Yeni değer dolu ve eskisinden farklı => filtre uygula
|
||
onFilter(newValue)
|
||
setPrevValue(newValue)
|
||
}}
|
||
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'}
|
||
className="text-sm"
|
||
onClick={() => {
|
||
setLayoutCount(1)
|
||
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 1 })
|
||
}}
|
||
title="1 Sütunda Göster"
|
||
>
|
||
1
|
||
</Button>
|
||
<Button
|
||
size="xs"
|
||
variant={layoutCount === 2 ? 'solid' : 'default'}
|
||
className="text-sm"
|
||
onClick={() => {
|
||
setLayoutCount(2)
|
||
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 2 })
|
||
}}
|
||
title="2 Sütunda Göster"
|
||
>
|
||
2
|
||
</Button>
|
||
<Button
|
||
size="xs"
|
||
variant={layoutCount === 3 ? 'solid' : 'default'}
|
||
className="text-sm"
|
||
onClick={() => {
|
||
setLayoutCount(3)
|
||
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 3 })
|
||
}}
|
||
title="3 Sütunda Göster"
|
||
>
|
||
3
|
||
</Button>
|
||
<Button
|
||
size="xs"
|
||
variant={layoutCount === 4 ? 'solid' : 'default'}
|
||
className="text-sm"
|
||
onClick={() => {
|
||
setLayoutCount(4)
|
||
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 4 })
|
||
}}
|
||
title="4 Sütunda Göster"
|
||
>
|
||
4
|
||
</Button>
|
||
<Button
|
||
size="xs"
|
||
variant={layoutCount === 5 ? 'solid' : 'default'}
|
||
className="text-sm"
|
||
onClick={() => {
|
||
setLayoutCount(5)
|
||
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 5 })
|
||
}}
|
||
title="5 Sütunda Göster"
|
||
>
|
||
5
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<Loading loading={true} />
|
||
) : data.length === 0 ? (
|
||
<div className="text-center py-12">
|
||
<FaSearch className="mx-auto h-10 w-10 text-gray-400" />
|
||
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-gray-200">
|
||
Uygun kayıt bulunamadı
|
||
</h3>
|
||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||
Farklı filtreler deneyin veya yeni bir kayıt ekleyin.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className={`bg-transparent grid grid-cols-1 lg:grid-cols-${layoutCount} gap-4`}>
|
||
{gridDataSource &&
|
||
data.map((row, idx) => {
|
||
const keyField = gridDto.gridOptions.keyFieldName
|
||
const rowId = row[keyField!]
|
||
|
||
return (
|
||
<CardItem
|
||
isSubForm={true}
|
||
key={rowId || idx}
|
||
row={row}
|
||
gridDto={gridDto}
|
||
listFormCode={listFormCode}
|
||
dataSource={gridDataSource}
|
||
refreshData={loadData}
|
||
getCachedLookupDataSource={getCachedLookupDataSource}
|
||
/>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (
|
||
<div className={classNames('flex items-center justify-between border-t-1 gap-4 mt-2')}>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs text-gray-600 dark:text-gray-300">
|
||
Toplam {totalCount} kayıt
|
||
</span>
|
||
<Select
|
||
size="xs"
|
||
menuPlacement="top"
|
||
value={pageSizeOptions.find((o) => o.value === pageSize)}
|
||
options={pageSizeOptions}
|
||
onChange={(selected) => onPageSizeSelect(selected as Option)}
|
||
/>
|
||
</div>
|
||
<Pagination
|
||
currentPage={currentPage}
|
||
total={totalCount}
|
||
pageSize={pageSize}
|
||
onChange={onPageChange}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Container>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default Card
|