Enhance ObjectCard and ObjectTable components with skeleton loading support
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Added an `isSkeleton` prop to ObjectCard for conditional rendering of skeleton loaders, improving user experience during data fetching. - Updated ObjectTable to manage skeleton boundaries, allowing for dynamic loading of pages based on visibility of skeleton cards. - Refactored scroll handling in ObjectTable to utilize IntersectionObserver for better performance and responsiveness.
This commit is contained in:
parent
3c634a90d0
commit
84b7197bb9
@ -1,10 +1,11 @@
|
||||
import { Descriptions, Card, Flex, Divider } from 'antd'
|
||||
import { Descriptions, Card, Flex, Divider, Skeleton } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import ObjectProperty from './ObjectProperty'
|
||||
import { createElement } from 'react'
|
||||
import Thumbnail from './Thumbnail'
|
||||
|
||||
const ObjectCard = ({
|
||||
isSkeleton = false,
|
||||
model,
|
||||
modelProperties,
|
||||
visibleColumns = {},
|
||||
@ -40,7 +41,9 @@ const ObjectCard = ({
|
||||
key: prop.name,
|
||||
label: prop.label,
|
||||
span: 2,
|
||||
children: (
|
||||
children: isSkeleton ? (
|
||||
<Skeleton.Input active size='small' style={{ height: '24px' }} />
|
||||
) : (
|
||||
<ObjectProperty
|
||||
{...prop}
|
||||
longId={false}
|
||||
@ -162,7 +165,8 @@ ObjectCard.propTypes = {
|
||||
isEditing: PropTypes.bool,
|
||||
rowActions: PropTypes.array,
|
||||
renderActions: PropTypes.func.isRequired,
|
||||
cardStyle: PropTypes.string
|
||||
cardStyle: PropTypes.string,
|
||||
isSkeleton: PropTypes.bool
|
||||
}
|
||||
|
||||
export default ObjectCard
|
||||
|
||||
@ -252,7 +252,7 @@ const ObjectTable = forwardRef(
|
||||
<QuestionCircleIcon />
|
||||
)
|
||||
}
|
||||
disabled={disabled}
|
||||
disabled={disabled || objectData?.isSkeleton}
|
||||
type={'text'}
|
||||
size={'small'}
|
||||
onClick={() => {
|
||||
@ -304,6 +304,21 @@ const ObjectTable = forwardRef(
|
||||
return SKELETON_HEIGHT * pageSize
|
||||
}, [pageSize])
|
||||
|
||||
const skeletonBoundaryIds = useMemo(() => {
|
||||
const firstPage = pages[0]
|
||||
const lastPage = pages[pages.length - 1]
|
||||
return {
|
||||
previous:
|
||||
firstPage?.isSkeletonPage && firstPage.items.length > 0
|
||||
? firstPage.items[firstPage.items.length - 1]._id
|
||||
: null,
|
||||
next:
|
||||
lastPage?.isSkeletonPage && lastPage.items[0]
|
||||
? lastPage.items[0]._id
|
||||
: null
|
||||
}
|
||||
}, [pages])
|
||||
|
||||
const createPageWindow = useCallback(
|
||||
(loadedPages, direction) => {
|
||||
const sortedPages = [...loadedPages].sort(
|
||||
@ -1101,37 +1116,39 @@ const ObjectTable = forwardRef(
|
||||
return () => cardsResizeObserverRef.current?.disconnect()
|
||||
}, [])
|
||||
|
||||
// Card view scroll handler
|
||||
useEffect(() => {
|
||||
// Card view: load when the leading skeleton card in a boundary page enters view
|
||||
useLayoutEffect(() => {
|
||||
if (!cards) return
|
||||
const container = cardsContainerNodeRef.current
|
||||
if (!container) return
|
||||
const scrollEl = container.querySelector('.simplebar-content-wrapper')
|
||||
if (!scrollEl) return
|
||||
|
||||
const handleCardsScroll = (e) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target
|
||||
const firstPage = pagesRef.current[0]
|
||||
const lastPage = pagesRef.current[pagesRef.current.length - 1]
|
||||
|
||||
if (
|
||||
lastPage?.isSkeletonPage &&
|
||||
scrollHeight - scrollTop - clientHeight < 100 &&
|
||||
!lazyLoading
|
||||
) {
|
||||
loadNextPage(scrollEl)
|
||||
} else if (
|
||||
firstPage?.isSkeletonPage &&
|
||||
scrollTop < 100 &&
|
||||
!lazyLoading
|
||||
) {
|
||||
loadPreviousPage(scrollEl)
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue
|
||||
const boundary = entry.target.dataset.skeletonBoundary
|
||||
if (boundary === 'next') {
|
||||
loadNextPage(scrollEl)
|
||||
} else if (boundary === 'previous') {
|
||||
loadPreviousPage(scrollEl)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
root: scrollEl,
|
||||
rootMargin: `${SCROLL_THRESHOLD}px`,
|
||||
threshold: 0
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
scrollEl.addEventListener('scroll', handleCardsScroll)
|
||||
return () => scrollEl.removeEventListener('scroll', handleCardsScroll)
|
||||
}, [cards, pages, lazyLoading, loadNextPage, loadPreviousPage])
|
||||
container
|
||||
.querySelectorAll('[data-skeleton-boundary]')
|
||||
.forEach((el) => observer.observe(el))
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [cards, skeletonBoundaryIds, loadNextPage, loadPreviousPage])
|
||||
|
||||
const renderCards = () => {
|
||||
return (
|
||||
@ -1146,9 +1163,20 @@ const ObjectTable = forwardRef(
|
||||
if (record?._id == undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const skeletonBoundary =
|
||||
record._id === skeletonBoundaryIds.next
|
||||
? 'next'
|
||||
: record._id === skeletonBoundaryIds.previous
|
||||
? 'previous'
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Col span={cardColSpan} key={record._id}>
|
||||
<div style={{ width: '100%', overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{ width: '100%', overflow: 'hidden' }}
|
||||
data-skeleton-boundary={skeletonBoundary}
|
||||
>
|
||||
<RowForm
|
||||
record={record}
|
||||
isEditing={isEditing}
|
||||
@ -1156,6 +1184,7 @@ const ObjectTable = forwardRef(
|
||||
>
|
||||
<Flex align={'center'} vertical gap={'middle'}>
|
||||
<ObjectCard
|
||||
isSkeleton={record?.isSkeleton || false}
|
||||
model={model}
|
||||
modelProperties={modelProperties}
|
||||
visibleColumns={visibleColumns}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user