diff --git a/ui/src/views/list/Card.tsx b/ui/src/views/list/Card.tsx index 5d9dc8ef..dfa330a6 100644 --- a/ui/src/views/list/Card.tsx +++ b/ui/src/views/list/Card.tsx @@ -40,10 +40,20 @@ const Card = (props: CardProps) => { const [pageSizeOptions, setPageSizeOptions] = useState([]) const [gridDataSource, setGridDataSource] = useState>() 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([]) @@ -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,14 +721,15 @@ const Card = (props: CardProps) => {

) : ( -
- {gridDataSource && - data.map((row, idx) => { + <> +
+ {gridDataSource && + 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} @@ -720,7 +759,18 @@ const Card = (props: CardProps) => { /> ) })} -
+
+ + {/* Progressive rendering sırasında loading göster */} + {isProgressiveRendering && renderedCount < data.length && ( +
+
+

+ Yükleniyor... ({renderedCount}/{data.length}) +

+
+ )} + )} {gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && ( diff --git a/ui/src/views/list/CardItem.tsx b/ui/src/views/list/CardItem.tsx index db3bffed..90c75a81 100644 --- a/ui/src/views/list/CardItem.tsx +++ b/ui/src/views/list/CardItem.tsx @@ -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(( }, [gridDto]) const [formData, setFormData] = useState(row) const refForm = useRef(null) + const cardElementRef = useRef(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(( return (
{ + // 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,14 +275,23 @@ const CardItem = forwardRef(( )}
- + {shouldRenderForm ? ( + + ) : ( +
+
+
+
+
+
+ )}
) @@ -248,4 +299,15 @@ const CardItem = forwardRef(( CardItem.displayName = 'CardItem' -export default CardItem \ No newline at end of file +// 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 + ) +}) \ No newline at end of file