Card görünüm loading problemi
This commit is contained in:
parent
928696a6c9
commit
56a35c2e73
2 changed files with 151 additions and 39 deletions
|
|
@ -40,10 +40,20 @@ const Card = (props: CardProps) => {
|
|||
const [pageSizeOptions, setPageSizeOptions] = useState<Option[]>([])
|
||||
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
|
||||
const { getLookupDataSource } = useLookupDataSource({ listFormCode })
|
||||
// Memoize getLookupDataSource to prevent recalculation on each render
|
||||
const memoizedGetLookupDataSource = useCallback(
|
||||
(editorOptions: any, colData: any, row?: any) => getLookupDataSource(editorOptions, colData, row),
|
||||
[getLookupDataSource]
|
||||
)
|
||||
const [layoutCount, setLayoutCount] = useState(4)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [prevValue, setPrevValue] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Progressive rendering state
|
||||
const [renderedCount, setRenderedCount] = useState(0)
|
||||
const [isProgressiveRendering, setIsProgressiveRendering] = useState(false)
|
||||
|
||||
const { checkPermission } = usePermission()
|
||||
const isPwaMode = usePWA()
|
||||
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
|
||||
|
|
@ -103,7 +113,7 @@ const Card = (props: CardProps) => {
|
|||
setCurrentPage(page)
|
||||
}
|
||||
|
||||
const toggleSort = (columnName: string) => {
|
||||
const toggleSort = useCallback((columnName: string) => {
|
||||
const newParams = new URLSearchParams(urlSearchParams.toString())
|
||||
|
||||
if (sortColumn === columnName) {
|
||||
|
|
@ -118,9 +128,9 @@ const Card = (props: CardProps) => {
|
|||
|
||||
setUrlSearchParams(newParams)
|
||||
setCurrentPage(1) // Reset to first page when sorting
|
||||
}
|
||||
}, [sortColumn, sortOrder, urlSearchParams])
|
||||
|
||||
const handleSelectAll = async (checked: boolean) => {
|
||||
const handleSelectAll = useCallback(async (checked: boolean) => {
|
||||
if (checked) {
|
||||
const keyField = gridDto?.gridOptions.keyFieldName
|
||||
const selectAllMode = gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase()
|
||||
|
|
@ -155,9 +165,9 @@ const Card = (props: CardProps) => {
|
|||
} else {
|
||||
setSelectedKeys(new Set())
|
||||
}
|
||||
}
|
||||
}, [gridDto, gridDataSource, totalCount, data])
|
||||
|
||||
const handleCardSelection = (key: any, checked: boolean) => {
|
||||
const handleCardSelection = useCallback((key: any, checked: boolean) => {
|
||||
const newSelection = new Set(selectedKeys)
|
||||
if (checked) {
|
||||
if (selectionMode === 'single') {
|
||||
|
|
@ -168,25 +178,26 @@ const Card = (props: CardProps) => {
|
|||
newSelection.delete(key)
|
||||
}
|
||||
setSelectedKeys(newSelection)
|
||||
}
|
||||
}, [selectedKeys, selectionMode])
|
||||
|
||||
const handleCardClick = (key: any, index: number, event: React.MouseEvent) => {
|
||||
const handleCardClick = useCallback((key: any, index: number, event: React.MouseEvent) => {
|
||||
setFocusedCardIndex(index)
|
||||
if (selectionMode !== 'none' && event.ctrlKey) {
|
||||
handleCardSelection(key, !selectedKeys.has(key))
|
||||
}
|
||||
}
|
||||
}, [selectionMode, handleCardSelection, selectedKeys])
|
||||
|
||||
const handleCardDoubleClick = (key: any, row: any) => {
|
||||
const handleCardDoubleClick = useCallback((key: any, row: any) => {
|
||||
// Navigate to edit page on double click
|
||||
const navigate = window.location
|
||||
if (!gridDto?.gridOptions.editingOptionDto?.allowUpdating) return
|
||||
|
||||
window.open(
|
||||
ROUTES_ENUM.protected.admin.formEdit
|
||||
.replace(':listFormCode', listFormCode)
|
||||
.replace(':id', key),
|
||||
isPwaMode ? '_self' : '_blank'
|
||||
)
|
||||
}
|
||||
}, [gridDto, listFormCode, isPwaMode])
|
||||
|
||||
const handleExport = async (format: 'xlsx' | 'csv' | 'pdf', selectedOnly: boolean = false) => {
|
||||
try {
|
||||
|
|
@ -288,7 +299,7 @@ const Card = (props: CardProps) => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (data.length === 0) return
|
||||
|
||||
const keyField = gridDto?.gridOptions.keyFieldName
|
||||
|
|
@ -342,7 +353,7 @@ const Card = (props: CardProps) => {
|
|||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [data, gridDto, focusedCardIndex, layoutCount, handleCardDoubleClick, selectionMode, handleCardSelection, selectedKeys])
|
||||
|
||||
const onFilter = useCallback(
|
||||
(value?: string) => {
|
||||
|
|
@ -413,6 +424,8 @@ const Card = (props: CardProps) => {
|
|||
const loadData = useCallback(() => {
|
||||
if (!gridDataSource) return
|
||||
setLoading(true)
|
||||
setIsProgressiveRendering(false)
|
||||
setRenderedCount(0)
|
||||
|
||||
const loadOptions = {
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
|
|
@ -426,6 +439,12 @@ const Card = (props: CardProps) => {
|
|||
setData(res.data)
|
||||
setTotalCount(res.totalCount || 0)
|
||||
setLoading(false)
|
||||
|
||||
// Veri yüklendi, şimdi progressive rendering başlat
|
||||
if (res.data && res.data.length > 0) {
|
||||
setIsProgressiveRendering(true)
|
||||
setRenderedCount(0)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
|
|
@ -442,7 +461,7 @@ const Card = (props: CardProps) => {
|
|||
setSelectedKeys(new Set())
|
||||
}
|
||||
}
|
||||
}, [gridDataSource, loadData, currentPage, gridDto])
|
||||
}, [gridDataSource, loadData, currentPage])
|
||||
|
||||
useEffect(() => {
|
||||
if (!gridDto) return
|
||||
|
|
@ -457,10 +476,29 @@ const Card = (props: CardProps) => {
|
|||
}, [gridDto, listFormCode, searchParams])
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length > 0) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
// Sadece yeni data yüklendiginde scroll yap - smooth yerine auto kullanılarak performans artırılıyor
|
||||
if (data.length > 0 && currentPage === 1) {
|
||||
window.scrollTo({ top: 0, behavior: 'auto' })
|
||||
}
|
||||
}, [data])
|
||||
}, [currentPage])
|
||||
|
||||
// Progressive rendering effect - kartları küçük batch'lerde render et
|
||||
useEffect(() => {
|
||||
if (!isProgressiveRendering || renderedCount >= data.length) {
|
||||
if (renderedCount >= data.length && isProgressiveRendering) {
|
||||
setIsProgressiveRendering(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Her seferde 3 kart render et - daha küçük batch size daha smooth experience
|
||||
const batchSize = 3
|
||||
const timeoutId = setTimeout(() => {
|
||||
setRenderedCount(prev => Math.min(prev + batchSize, data.length))
|
||||
}, 10) // 10ms gecikme - UI'ın nefes almasını sağlar
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}, [isProgressiveRendering, renderedCount, data.length])
|
||||
|
||||
if (!gridDto) return null
|
||||
|
||||
|
|
@ -683,6 +721,7 @@ const Card = (props: CardProps) => {
|
|||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className="bg-transparent grid gap-4"
|
||||
style={{
|
||||
|
|
@ -690,7 +729,7 @@ const Card = (props: CardProps) => {
|
|||
}}
|
||||
>
|
||||
{gridDataSource &&
|
||||
data.map((row, idx) => {
|
||||
data.slice(0, isProgressiveRendering ? renderedCount : data.length).map((row, idx) => {
|
||||
const keyField = gridDto.gridOptions.keyFieldName
|
||||
const rowId = row[keyField!]
|
||||
const isSelected = selectedKeys.has(rowId)
|
||||
|
|
@ -707,7 +746,7 @@ const Card = (props: CardProps) => {
|
|||
listFormCode={listFormCode}
|
||||
dataSource={gridDataSource}
|
||||
refreshData={loadData}
|
||||
getCachedLookupDataSource={getLookupDataSource}
|
||||
getCachedLookupDataSource={memoizedGetLookupDataSource}
|
||||
isSelected={isSelected}
|
||||
isFocused={isFocused}
|
||||
isHovered={isHovered}
|
||||
|
|
@ -721,6 +760,17 @@ const Card = (props: CardProps) => {
|
|||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Progressive rendering sırasında loading göster */}
|
||||
{isProgressiveRendering && renderedCount < data.length && (
|
||||
<div className="text-center py-4">
|
||||
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
Yükleniyor... ({renderedCount}/{data.length})
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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 } from "react"
|
||||
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"
|
||||
|
|
@ -66,15 +66,48 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
|||
}, [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
|
||||
// Form Items - memoized to prevent recalculation on every render
|
||||
const formItems: GroupItem[] = useMemo(() => {
|
||||
if (!gridDto) return []
|
||||
|
||||
|
|
@ -165,7 +198,16 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
|||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
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}
|
||||
|
|
@ -233,6 +275,7 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
|||
)}
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
{shouldRenderForm ? (
|
||||
<FormDevExpress
|
||||
listFormCode={listFormCode}
|
||||
mode="view"
|
||||
|
|
@ -241,6 +284,14 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
|||
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>
|
||||
)
|
||||
|
|
@ -248,4 +299,15 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
|||
|
||||
CardItem.displayName = 'CardItem'
|
||||
|
||||
export default 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
|
||||
)
|
||||
})
|
||||
Loading…
Reference in a new issue