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 PropTypes from 'prop-types'
|
||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
import { createElement } from 'react'
|
import { createElement } from 'react'
|
||||||
import Thumbnail from './Thumbnail'
|
import Thumbnail from './Thumbnail'
|
||||||
|
|
||||||
const ObjectCard = ({
|
const ObjectCard = ({
|
||||||
|
isSkeleton = false,
|
||||||
model,
|
model,
|
||||||
modelProperties,
|
modelProperties,
|
||||||
visibleColumns = {},
|
visibleColumns = {},
|
||||||
@ -40,7 +41,9 @@ const ObjectCard = ({
|
|||||||
key: prop.name,
|
key: prop.name,
|
||||||
label: prop.label,
|
label: prop.label,
|
||||||
span: 2,
|
span: 2,
|
||||||
children: (
|
children: isSkeleton ? (
|
||||||
|
<Skeleton.Input active size='small' style={{ height: '24px' }} />
|
||||||
|
) : (
|
||||||
<ObjectProperty
|
<ObjectProperty
|
||||||
{...prop}
|
{...prop}
|
||||||
longId={false}
|
longId={false}
|
||||||
@ -162,7 +165,8 @@ ObjectCard.propTypes = {
|
|||||||
isEditing: PropTypes.bool,
|
isEditing: PropTypes.bool,
|
||||||
rowActions: PropTypes.array,
|
rowActions: PropTypes.array,
|
||||||
renderActions: PropTypes.func.isRequired,
|
renderActions: PropTypes.func.isRequired,
|
||||||
cardStyle: PropTypes.string
|
cardStyle: PropTypes.string,
|
||||||
|
isSkeleton: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectCard
|
export default ObjectCard
|
||||||
|
|||||||
@ -252,7 +252,7 @@ const ObjectTable = forwardRef(
|
|||||||
<QuestionCircleIcon />
|
<QuestionCircleIcon />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={disabled}
|
disabled={disabled || objectData?.isSkeleton}
|
||||||
type={'text'}
|
type={'text'}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -304,6 +304,21 @@ const ObjectTable = forwardRef(
|
|||||||
return SKELETON_HEIGHT * pageSize
|
return SKELETON_HEIGHT * pageSize
|
||||||
}, [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(
|
const createPageWindow = useCallback(
|
||||||
(loadedPages, direction) => {
|
(loadedPages, direction) => {
|
||||||
const sortedPages = [...loadedPages].sort(
|
const sortedPages = [...loadedPages].sort(
|
||||||
@ -1101,37 +1116,39 @@ const ObjectTable = forwardRef(
|
|||||||
return () => cardsResizeObserverRef.current?.disconnect()
|
return () => cardsResizeObserverRef.current?.disconnect()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Card view scroll handler
|
// Card view: load when the leading skeleton card in a boundary page enters view
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!cards) return
|
if (!cards) return
|
||||||
const container = cardsContainerNodeRef.current
|
const container = cardsContainerNodeRef.current
|
||||||
if (!container) return
|
if (!container) return
|
||||||
const scrollEl = container.querySelector('.simplebar-content-wrapper')
|
const scrollEl = container.querySelector('.simplebar-content-wrapper')
|
||||||
if (!scrollEl) return
|
if (!scrollEl) return
|
||||||
|
|
||||||
const handleCardsScroll = (e) => {
|
const observer = new IntersectionObserver(
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.target
|
(entries) => {
|
||||||
const firstPage = pagesRef.current[0]
|
for (const entry of entries) {
|
||||||
const lastPage = pagesRef.current[pagesRef.current.length - 1]
|
if (!entry.isIntersecting) continue
|
||||||
|
const boundary = entry.target.dataset.skeletonBoundary
|
||||||
if (
|
if (boundary === 'next') {
|
||||||
lastPage?.isSkeletonPage &&
|
loadNextPage(scrollEl)
|
||||||
scrollHeight - scrollTop - clientHeight < 100 &&
|
} else if (boundary === 'previous') {
|
||||||
!lazyLoading
|
loadPreviousPage(scrollEl)
|
||||||
) {
|
}
|
||||||
loadNextPage(scrollEl)
|
}
|
||||||
} else if (
|
},
|
||||||
firstPage?.isSkeletonPage &&
|
{
|
||||||
scrollTop < 100 &&
|
root: scrollEl,
|
||||||
!lazyLoading
|
rootMargin: `${SCROLL_THRESHOLD}px`,
|
||||||
) {
|
threshold: 0
|
||||||
loadPreviousPage(scrollEl)
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
scrollEl.addEventListener('scroll', handleCardsScroll)
|
container
|
||||||
return () => scrollEl.removeEventListener('scroll', handleCardsScroll)
|
.querySelectorAll('[data-skeleton-boundary]')
|
||||||
}, [cards, pages, lazyLoading, loadNextPage, loadPreviousPage])
|
.forEach((el) => observer.observe(el))
|
||||||
|
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [cards, skeletonBoundaryIds, loadNextPage, loadPreviousPage])
|
||||||
|
|
||||||
const renderCards = () => {
|
const renderCards = () => {
|
||||||
return (
|
return (
|
||||||
@ -1146,9 +1163,20 @@ const ObjectTable = forwardRef(
|
|||||||
if (record?._id == undefined) {
|
if (record?._id == undefined) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const skeletonBoundary =
|
||||||
|
record._id === skeletonBoundaryIds.next
|
||||||
|
? 'next'
|
||||||
|
: record._id === skeletonBoundaryIds.previous
|
||||||
|
? 'previous'
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col span={cardColSpan} key={record._id}>
|
<Col span={cardColSpan} key={record._id}>
|
||||||
<div style={{ width: '100%', overflow: 'hidden' }}>
|
<div
|
||||||
|
style={{ width: '100%', overflow: 'hidden' }}
|
||||||
|
data-skeleton-boundary={skeletonBoundary}
|
||||||
|
>
|
||||||
<RowForm
|
<RowForm
|
||||||
record={record}
|
record={record}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
@ -1156,6 +1184,7 @@ const ObjectTable = forwardRef(
|
|||||||
>
|
>
|
||||||
<Flex align={'center'} vertical gap={'middle'}>
|
<Flex align={'center'} vertical gap={'middle'}>
|
||||||
<ObjectCard
|
<ObjectCard
|
||||||
|
isSkeleton={record?.isSkeleton || false}
|
||||||
model={model}
|
model={model}
|
||||||
modelProperties={modelProperties}
|
modelProperties={modelProperties}
|
||||||
visibleColumns={visibleColumns}
|
visibleColumns={visibleColumns}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user