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 [pageSizeOptions, setPageSizeOptions] = useState<Option[]>([])
|
||||||
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
|
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
|
||||||
const { getLookupDataSource } = useLookupDataSource({ listFormCode })
|
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 [layoutCount, setLayoutCount] = useState(4)
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
const [prevValue, setPrevValue] = useState('')
|
const [prevValue, setPrevValue] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
// Progressive rendering state
|
||||||
|
const [renderedCount, setRenderedCount] = useState(0)
|
||||||
|
const [isProgressiveRendering, setIsProgressiveRendering] = useState(false)
|
||||||
|
|
||||||
const { checkPermission } = usePermission()
|
const { checkPermission } = usePermission()
|
||||||
const isPwaMode = usePWA()
|
const isPwaMode = usePWA()
|
||||||
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
|
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
|
||||||
|
|
@ -103,7 +113,7 @@ const Card = (props: CardProps) => {
|
||||||
setCurrentPage(page)
|
setCurrentPage(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleSort = (columnName: string) => {
|
const toggleSort = useCallback((columnName: string) => {
|
||||||
const newParams = new URLSearchParams(urlSearchParams.toString())
|
const newParams = new URLSearchParams(urlSearchParams.toString())
|
||||||
|
|
||||||
if (sortColumn === columnName) {
|
if (sortColumn === columnName) {
|
||||||
|
|
@ -118,9 +128,9 @@ const Card = (props: CardProps) => {
|
||||||
|
|
||||||
setUrlSearchParams(newParams)
|
setUrlSearchParams(newParams)
|
||||||
setCurrentPage(1) // Reset to first page when sorting
|
setCurrentPage(1) // Reset to first page when sorting
|
||||||
}
|
}, [sortColumn, sortOrder, urlSearchParams])
|
||||||
|
|
||||||
const handleSelectAll = async (checked: boolean) => {
|
const handleSelectAll = useCallback(async (checked: boolean) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
const keyField = gridDto?.gridOptions.keyFieldName
|
const keyField = gridDto?.gridOptions.keyFieldName
|
||||||
const selectAllMode = gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase()
|
const selectAllMode = gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase()
|
||||||
|
|
@ -155,9 +165,9 @@ const Card = (props: CardProps) => {
|
||||||
} else {
|
} else {
|
||||||
setSelectedKeys(new Set())
|
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)
|
const newSelection = new Set(selectedKeys)
|
||||||
if (checked) {
|
if (checked) {
|
||||||
if (selectionMode === 'single') {
|
if (selectionMode === 'single') {
|
||||||
|
|
@ -168,25 +178,26 @@ const Card = (props: CardProps) => {
|
||||||
newSelection.delete(key)
|
newSelection.delete(key)
|
||||||
}
|
}
|
||||||
setSelectedKeys(newSelection)
|
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)
|
setFocusedCardIndex(index)
|
||||||
if (selectionMode !== 'none' && event.ctrlKey) {
|
if (selectionMode !== 'none' && event.ctrlKey) {
|
||||||
handleCardSelection(key, !selectedKeys.has(key))
|
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
|
// Navigate to edit page on double click
|
||||||
const navigate = window.location
|
if (!gridDto?.gridOptions.editingOptionDto?.allowUpdating) return
|
||||||
|
|
||||||
window.open(
|
window.open(
|
||||||
ROUTES_ENUM.protected.admin.formEdit
|
ROUTES_ENUM.protected.admin.formEdit
|
||||||
.replace(':listFormCode', listFormCode)
|
.replace(':listFormCode', listFormCode)
|
||||||
.replace(':id', key),
|
.replace(':id', key),
|
||||||
isPwaMode ? '_self' : '_blank'
|
isPwaMode ? '_self' : '_blank'
|
||||||
)
|
)
|
||||||
}
|
}, [gridDto, listFormCode, isPwaMode])
|
||||||
|
|
||||||
const handleExport = async (format: 'xlsx' | 'csv' | 'pdf', selectedOnly: boolean = false) => {
|
const handleExport = async (format: 'xlsx' | 'csv' | 'pdf', selectedOnly: boolean = false) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -288,7 +299,7 @@ const Card = (props: CardProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
if (data.length === 0) return
|
if (data.length === 0) return
|
||||||
|
|
||||||
const keyField = gridDto?.gridOptions.keyFieldName
|
const keyField = gridDto?.gridOptions.keyFieldName
|
||||||
|
|
@ -342,7 +353,7 @@ const Card = (props: CardProps) => {
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}, [data, gridDto, focusedCardIndex, layoutCount, handleCardDoubleClick, selectionMode, handleCardSelection, selectedKeys])
|
||||||
|
|
||||||
const onFilter = useCallback(
|
const onFilter = useCallback(
|
||||||
(value?: string) => {
|
(value?: string) => {
|
||||||
|
|
@ -413,6 +424,8 @@ const Card = (props: CardProps) => {
|
||||||
const loadData = useCallback(() => {
|
const loadData = useCallback(() => {
|
||||||
if (!gridDataSource) return
|
if (!gridDataSource) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
setIsProgressiveRendering(false)
|
||||||
|
setRenderedCount(0)
|
||||||
|
|
||||||
const loadOptions = {
|
const loadOptions = {
|
||||||
skip: (currentPage - 1) * pageSize,
|
skip: (currentPage - 1) * pageSize,
|
||||||
|
|
@ -426,6 +439,12 @@ const Card = (props: CardProps) => {
|
||||||
setData(res.data)
|
setData(res.data)
|
||||||
setTotalCount(res.totalCount || 0)
|
setTotalCount(res.totalCount || 0)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
|
// Veri yüklendi, şimdi progressive rendering başlat
|
||||||
|
if (res.data && res.data.length > 0) {
|
||||||
|
setIsProgressiveRendering(true)
|
||||||
|
setRenderedCount(0)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
@ -442,7 +461,7 @@ const Card = (props: CardProps) => {
|
||||||
setSelectedKeys(new Set())
|
setSelectedKeys(new Set())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [gridDataSource, loadData, currentPage, gridDto])
|
}, [gridDataSource, loadData, currentPage])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gridDto) return
|
if (!gridDto) return
|
||||||
|
|
@ -457,10 +476,29 @@ const Card = (props: CardProps) => {
|
||||||
}, [gridDto, listFormCode, searchParams])
|
}, [gridDto, listFormCode, searchParams])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data.length > 0) {
|
// Sadece yeni data yüklendiginde scroll yap - smooth yerine auto kullanılarak performans artırılıyor
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
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
|
if (!gridDto) return null
|
||||||
|
|
||||||
|
|
@ -683,6 +721,7 @@ const Card = (props: CardProps) => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
className="bg-transparent grid gap-4"
|
className="bg-transparent grid gap-4"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -690,7 +729,7 @@ const Card = (props: CardProps) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{gridDataSource &&
|
{gridDataSource &&
|
||||||
data.map((row, idx) => {
|
data.slice(0, isProgressiveRendering ? renderedCount : data.length).map((row, idx) => {
|
||||||
const keyField = gridDto.gridOptions.keyFieldName
|
const keyField = gridDto.gridOptions.keyFieldName
|
||||||
const rowId = row[keyField!]
|
const rowId = row[keyField!]
|
||||||
const isSelected = selectedKeys.has(rowId)
|
const isSelected = selectedKeys.has(rowId)
|
||||||
|
|
@ -707,7 +746,7 @@ const Card = (props: CardProps) => {
|
||||||
listFormCode={listFormCode}
|
listFormCode={listFormCode}
|
||||||
dataSource={gridDataSource}
|
dataSource={gridDataSource}
|
||||||
refreshData={loadData}
|
refreshData={loadData}
|
||||||
getCachedLookupDataSource={getLookupDataSource}
|
getCachedLookupDataSource={memoizedGetLookupDataSource}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isFocused={isFocused}
|
isFocused={isFocused}
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
|
|
@ -721,6 +760,17 @@ const Card = (props: CardProps) => {
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</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 && (
|
{gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useLocalization } from "@/utils/hooks/useLocalization"
|
||||||
import { usePermission } from "@/utils/hooks/usePermission"
|
import { usePermission } from "@/utils/hooks/usePermission"
|
||||||
import { usePWA } from "@/utils/hooks/usePWA"
|
import { usePWA } from "@/utils/hooks/usePWA"
|
||||||
import CustomStore from "devextreme/data/custom_store"
|
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 { useNavigate } from "react-router-dom"
|
||||||
import { GroupItem } from 'devextreme/ui/form'
|
import { GroupItem } from 'devextreme/ui/form'
|
||||||
import { PermissionResults, SimpleItemWithColData } from "../form/types"
|
import { PermissionResults, SimpleItemWithColData } from "../form/types"
|
||||||
|
|
@ -66,15 +66,48 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
||||||
}, [gridDto])
|
}, [gridDto])
|
||||||
const [formData, setFormData] = useState(row)
|
const [formData, setFormData] = useState(row)
|
||||||
const refForm = useRef<FormRef>(null)
|
const refForm = useRef<FormRef>(null)
|
||||||
|
const cardElementRef = useRef<HTMLDivElement | null>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const { checkPermission } = usePermission()
|
const { checkPermission } = usePermission()
|
||||||
const isPwaMode = usePWA()
|
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 keyField = gridDto.gridOptions.keyFieldName
|
||||||
const rowId = row[keyField!]
|
const rowId = row[keyField!]
|
||||||
|
|
||||||
// Form Items
|
// Form Items - memoized to prevent recalculation on every render
|
||||||
const formItems: GroupItem[] = useMemo(() => {
|
const formItems: GroupItem[] = useMemo(() => {
|
||||||
if (!gridDto) return []
|
if (!gridDto) return []
|
||||||
|
|
||||||
|
|
@ -165,7 +198,16 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
tabIndex={tabIndex}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onDoubleClick={onDoubleClick}
|
onDoubleClick={onDoubleClick}
|
||||||
|
|
@ -233,6 +275,7 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
|
{shouldRenderForm ? (
|
||||||
<FormDevExpress
|
<FormDevExpress
|
||||||
listFormCode={listFormCode}
|
listFormCode={listFormCode}
|
||||||
mode="view"
|
mode="view"
|
||||||
|
|
@ -241,6 +284,14 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
||||||
formItems={formItems}
|
formItems={formItems}
|
||||||
setFormData={setFormData}
|
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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -248,4 +299,15 @@ const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
||||||
|
|
||||||
CardItem.displayName = 'CardItem'
|
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