314 lines
No EOL
10 KiB
TypeScript
314 lines
No EOL
10 KiB
TypeScript
import { EditingFormItemDto, GridDto, PlatformEditorTypes } from "@/proxy/form/models"
|
||
import { useLocalization } from "@/utils/hooks/useLocalization"
|
||
import { usePermission } from "@/utils/hooks/usePermission"
|
||
import { usePWA } from "@/utils/hooks/usePWA"
|
||
import CustomStore from "devextreme/data/custom_store"
|
||
import { useMemo, useRef, useState, forwardRef, memo, useEffect } from "react"
|
||
import { useNavigate } from "react-router-dom"
|
||
import { GroupItem } from 'devextreme/ui/form'
|
||
import { PermissionResults, SimpleItemWithColData } from "../form/types"
|
||
import { captionize } from 'devextreme/core/utils/inflector'
|
||
import { ROUTES_ENUM } from "@/routes/route.constant"
|
||
import FormButtons from "../form/FormButtons"
|
||
import FormDevExpress from "../form/FormDevExpress"
|
||
import { FormRef } from "devextreme-react/cjs/form"
|
||
import { Checkbox } from "@/components/ui"
|
||
import classNames from "classnames"
|
||
|
||
interface CardItemProps {
|
||
isSubForm?: boolean
|
||
row: any
|
||
gridDto: GridDto
|
||
listFormCode: string
|
||
dataSource: CustomStore
|
||
refreshData: () => void
|
||
getCachedLookupDataSource: (editorOptions: any, colData: any, row?: any) => any
|
||
// Selection and interaction props
|
||
isSelected?: boolean
|
||
isFocused?: boolean
|
||
isHovered?: boolean
|
||
onSelectionChange?: (checked: boolean) => void
|
||
onClick?: (e: React.MouseEvent) => void
|
||
onDoubleClick?: () => void
|
||
onMouseEnter?: () => void
|
||
onMouseLeave?: () => void
|
||
tabIndex?: number
|
||
}
|
||
|
||
const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
||
{
|
||
isSubForm,
|
||
row,
|
||
gridDto,
|
||
listFormCode,
|
||
dataSource,
|
||
refreshData,
|
||
getCachedLookupDataSource,
|
||
isSelected = false,
|
||
isFocused = false,
|
||
isHovered = false,
|
||
onSelectionChange,
|
||
onClick,
|
||
onDoubleClick,
|
||
onMouseEnter,
|
||
onMouseLeave,
|
||
tabIndex = -1,
|
||
},
|
||
ref
|
||
) => {
|
||
// Selection mode'u gridDto'dan al
|
||
const selectionMode = useMemo(() => {
|
||
if (!gridDto?.gridOptions?.selectionDto?.mode) return 'none'
|
||
const mode = gridDto.gridOptions.selectionDto.mode.toLowerCase()
|
||
if (mode === 'single') return 'single'
|
||
if (mode === 'multiple') return 'multiple'
|
||
return 'none'
|
||
}, [gridDto])
|
||
const [formData, setFormData] = useState(row)
|
||
const refForm = useRef<FormRef>(null)
|
||
const cardElementRef = useRef<HTMLDivElement | null>(null)
|
||
const navigate = useNavigate()
|
||
const { translate } = useLocalization()
|
||
const { checkPermission } = usePermission()
|
||
const isPwaMode = usePWA()
|
||
|
||
// Lazy load form with Intersection Observer - sadece görünür kartları render et
|
||
const [shouldRenderForm, setShouldRenderForm] = useState(false)
|
||
|
||
useEffect(() => {
|
||
const element = cardElementRef.current
|
||
if (!element) return
|
||
|
||
// Intersection Observer ile görünürlük kontrolü
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
entries.forEach((entry) => {
|
||
if (entry.isIntersecting) {
|
||
// Kart görünür hale geldiğinde form'u render et
|
||
setShouldRenderForm(true)
|
||
// Bir kez render edildikten sonra observer'ı kaldır
|
||
observer.disconnect()
|
||
}
|
||
})
|
||
},
|
||
{
|
||
rootMargin: '50px', // 50px önceden yüklemeye başla
|
||
threshold: 0.01, // %1 görünür olduğunda tetikle
|
||
}
|
||
)
|
||
|
||
observer.observe(element)
|
||
|
||
return () => {
|
||
observer.disconnect()
|
||
}
|
||
}, [])
|
||
|
||
const keyField = gridDto.gridOptions.keyFieldName
|
||
const rowId = row[keyField!]
|
||
|
||
// Form Items - memoized to prevent recalculation on every render
|
||
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,
|
||
label: { text: colData?.captionName ? translate('::' + colData?.captionName) : 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,
|
||
editorScript: i.editorScript,
|
||
}
|
||
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, translate])
|
||
|
||
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
|
||
ref={(element) => {
|
||
// Forward ref to parent
|
||
if (typeof ref === 'function') {
|
||
ref(element)
|
||
} else if (ref) {
|
||
ref.current = element
|
||
}
|
||
// Also save to local ref for Intersection Observer
|
||
cardElementRef.current = element
|
||
}}
|
||
tabIndex={tabIndex}
|
||
onClick={onClick}
|
||
onDoubleClick={onDoubleClick}
|
||
onMouseEnter={onMouseEnter}
|
||
onMouseLeave={onMouseLeave}
|
||
className={classNames(
|
||
"bg-white dark:bg-neutral-800 border flex flex-col transition-all duration-200 outline-none",
|
||
{
|
||
"border-blue-500 shadow-lg ring-2 ring-blue-300": isSelected,
|
||
"border-blue-400 shadow-md": isFocused && !isSelected,
|
||
"border-gray-300 dark:border-neutral-600": !isSelected && !isFocused,
|
||
"shadow-md transform scale-[1.02]": isHovered,
|
||
"cursor-pointer": selectionMode !== 'none',
|
||
}
|
||
)}
|
||
role="article"
|
||
aria-selected={isSelected}
|
||
aria-label={`Card ${rowId}`}
|
||
>
|
||
<div
|
||
className={`flex items-center pt-2 px-2 gap-2 ${isSubForm ? 'justify-between' : 'justify-between'}`}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{selectionMode !== 'none' && onSelectionChange && (
|
||
<div
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onSelectionChange(!isSelected)
|
||
}}
|
||
>
|
||
<Checkbox
|
||
checked={isSelected}
|
||
onChange={(checked: boolean) => {
|
||
onSelectionChange(checked)
|
||
}}
|
||
className="cursor-pointer"
|
||
aria-label="Select card"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{!isSubForm && (
|
||
<h3>{translate('::' + gridDto?.gridOptions.title)}</h3>
|
||
)}
|
||
</div>
|
||
|
||
{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">
|
||
{shouldRenderForm ? (
|
||
<FormDevExpress
|
||
listFormCode={listFormCode}
|
||
mode="view"
|
||
refForm={refForm}
|
||
formData={formData}
|
||
formItems={formItems}
|
||
setFormData={setFormData}
|
||
/>
|
||
) : (
|
||
<div className="p-4">
|
||
<div className="animate-pulse space-y-2">
|
||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
})
|
||
|
||
CardItem.displayName = 'CardItem'
|
||
|
||
// Memoize to prevent unnecessary re-renders when parent re-renders
|
||
export default memo(CardItem, (prevProps, nextProps) => {
|
||
// Custom comparison to prevent re-render when not needed
|
||
return (
|
||
prevProps.row === nextProps.row &&
|
||
prevProps.isSelected === nextProps.isSelected &&
|
||
prevProps.isFocused === nextProps.isFocused &&
|
||
prevProps.isHovered === nextProps.isHovered &&
|
||
prevProps.gridDto === nextProps.gridDto &&
|
||
prevProps.listFormCode === nextProps.listFormCode
|
||
)
|
||
}) |