Enhance LoadingPlaceholder and Object Components with Lazy Loading Support

- Updated LoadingPlaceholder to accept new props for border and height customization, improving flexibility in loading states.
- Integrated lazy loading functionality across ObjectCard, ObjectKanban, ObjectKanbanColumn, ObjectKanbanHeader, and ObjectTable components, enhancing user experience during data fetching.
- Adjusted rendering logic in ObjectTable to display LoadingPlaceholder when data is being fetched, providing clearer visual feedback to users.
- Refactored prop types in relevant components to include lazy loading options, ensuring consistent API across components.
This commit is contained in:
Tom Butcher 2026-09-03 03:53:31 +01:00
parent dc237eecdd
commit cb49baaf93
6 changed files with 149 additions and 68 deletions

View File

@ -4,13 +4,29 @@ import PropTypes from 'prop-types'
const { Text } = Typography
const LoadingPlaceholder = ({ message, hasBackground = true }) => {
const LoadingPlaceholder = ({
message,
hasBackground = true,
hasBorder = true,
height = undefined
}) => {
return (
<Card
size='small'
style={{
background: hasBackground == false ? 'transparent' : undefined,
border: hasBackground == false ? '1px solid rgb(0 0 0 / 7%)' : undefined
border:
hasBackground == false && hasBorder == false
? '1px solid rgb(0 0 0 / 7%)'
: hasBackground == true && hasBorder == false
? 'none'
: undefined
}}
styles={{
body: {
height: height == undefined ? undefined : height
}
}}
>
<Flex
@ -32,7 +48,9 @@ const LoadingPlaceholder = ({ message, hasBackground = true }) => {
LoadingPlaceholder.propTypes = {
message: PropTypes.string.isRequired,
hasBackground: PropTypes.bool
hasBackground: PropTypes.bool,
hasBorder: PropTypes.bool,
height: PropTypes.string
}
export default LoadingPlaceholder

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import ObjectProperty from './ObjectProperty'
import { createElement } from 'react'
import Thumbnail from './Thumbnail'
import { LoadingOutlined } from '@ant-design/icons'
const ObjectCard = ({
isSkeleton = false,
@ -13,6 +14,7 @@ const ObjectCard = ({
isEditing = false,
rowActions = [],
renderActions,
lazyLoading = false,
cardStyle = 'borderless'
}) => {
const descriptionItems = []
@ -59,7 +61,7 @@ const ObjectCard = ({
var actions = undefined
if (rowActions.length > 0) {
actions = renderActions(record)
actions = renderActions(record, lazyLoading)
}
const primaryDescriptionItems = thumbnailPresent
@ -146,8 +148,20 @@ const ObjectCard = ({
{actions && (
<>
<Divider style={{ margin: '4px 0 0 0' }} />
<Flex align='flex-end' gap={10}>
{actions}
<Flex
align='center'
gap={10}
style={{ width: '100%' }}
justify='space-between'
>
<Flex align='center' gap={10}>
{actions}
</Flex>
{lazyLoading && (
<LoadingOutlined spin style={{ marginRight: 4 }} />
)}
</Flex>
</>
)}
@ -166,7 +180,8 @@ ObjectCard.propTypes = {
rowActions: PropTypes.array,
renderActions: PropTypes.func.isRequired,
cardStyle: PropTypes.string,
isSkeleton: PropTypes.bool
isSkeleton: PropTypes.bool,
lazyLoading: PropTypes.bool
}
export default ObjectCard

View File

@ -101,6 +101,7 @@ const ObjectKanban = forwardRef(
baseFilter = {},
masterFilter = {},
sorter = {},
lazyLoading = false,
pageSize = 25,
model,
modelProperties,
@ -321,6 +322,7 @@ const ObjectKanban = forwardRef(
skeletonColumnCount={
showSkeleton ? skeletonLayout.columnCount : 0
}
lazyLoading={showSkeleton || lazyLoading}
/>
<div className='objectKanbanKanbanBody' ref={kanbanBodyRef}>
{!showSkeleton && (
@ -410,6 +412,7 @@ ObjectKanban.propTypes = {
baseFilter: PropTypes.object,
masterFilter: PropTypes.object,
sorter: PropTypes.object,
lazyLoading: PropTypes.bool,
pageSize: PropTypes.number,
model: PropTypes.object.isRequired,
modelProperties: PropTypes.array.isRequired,

View File

@ -33,6 +33,7 @@ const ObjectKanbanColumn = forwardRef(
visibleColumns = {},
isEditing = false,
rowActions = [],
lazyLoading = false,
renderActions,
scrollElement = null
},
@ -358,60 +359,63 @@ const ObjectKanbanColumn = forwardRef(
[loadBoundaryPage]
)
const loadInitialPage = useCallback(async ({ silent = false } = {}) => {
dataLoadGenerationRef.current += 1
loadingPagesRef.current.clear()
pendingScrollAnchorRef.current = null
const generation = dataLoadGenerationRef.current
const filter = {
...masterFilterRef.current,
...baseFilterRef.current,
[categoryProperty]: toCategoryFilterValue(categoryValue)
}
const sorter = sorterRef.current
if (!silent) {
pagesRef.current = []
setPages([])
setLoading(true)
const skeletonPage = createSkeletonPage(1)
setTablePages([skeletonPage])
} else {
setLoading(false)
}
try {
const firstResult = await fetchPage(1, { filter, sorter })
if (isStaleDataLoad(generation)) return
const loadedPages = [
{
pageNum: 1,
items: firstResult.data || [],
hasMore: firstResult.hasMore
}
]
if (!isStaleDataLoad(generation)) {
setTablePages(createPageWindow(loadedPages, 'next'))
const loadInitialPage = useCallback(
async ({ silent = false } = {}) => {
dataLoadGenerationRef.current += 1
loadingPagesRef.current.clear()
pendingScrollAnchorRef.current = null
const generation = dataLoadGenerationRef.current
const filter = {
...masterFilterRef.current,
...baseFilterRef.current,
[categoryProperty]: toCategoryFilterValue(categoryValue)
}
} catch {
if (!silent && !isStaleDataLoad(generation)) {
setTablePages([])
}
} finally {
if (!silent && !isStaleDataLoad(generation)) {
const sorter = sorterRef.current
if (!silent) {
pagesRef.current = []
setPages([])
setLoading(true)
const skeletonPage = createSkeletonPage(1)
setTablePages([skeletonPage])
} else {
setLoading(false)
}
}
}, [
categoryProperty,
categoryValue,
createPageWindow,
createSkeletonPage,
fetchPage,
isStaleDataLoad,
setTablePages
])
try {
const firstResult = await fetchPage(1, { filter, sorter })
if (isStaleDataLoad(generation)) return
const loadedPages = [
{
pageNum: 1,
items: firstResult.data || [],
hasMore: firstResult.hasMore
}
]
if (!isStaleDataLoad(generation)) {
setTablePages(createPageWindow(loadedPages, 'next'))
}
} catch {
if (!silent && !isStaleDataLoad(generation)) {
setTablePages([])
}
} finally {
if (!silent && !isStaleDataLoad(generation)) {
setLoading(false)
}
}
},
[
categoryProperty,
categoryValue,
createPageWindow,
createSkeletonPage,
fetchPage,
isStaleDataLoad,
setTablePages
]
)
const reloadLoadedPages = useCallback(async () => {
const loadedPages = pagesRef.current.filter(
@ -533,6 +537,7 @@ const ObjectKanbanColumn = forwardRef(
rowActions={rowActions}
renderActions={renderActions}
cardStyle='bordered'
lazyLoading={lazyLoading}
/>
</div>
)
@ -560,6 +565,7 @@ ObjectKanbanColumn.propTypes = {
visibleColumns: PropTypes.object,
isEditing: PropTypes.bool,
rowActions: PropTypes.array,
lazyLoading: PropTypes.bool,
renderActions: PropTypes.func,
scrollElement: PropTypes.object
}

View File

@ -2,20 +2,22 @@ import { Flex, Skeleton } from 'antd'
import PropTypes from 'prop-types'
import ObjectProperty from './ObjectProperty'
import { getCategoryValueKey } from './viewModeUtils'
import { LoadingOutlined } from '@ant-design/icons'
const ObjectKanbanHeader = ({
categoryValues,
categoryProperty,
categoryPropertyDef,
trackRef,
skeletonColumnCount = 0
skeletonColumnCount = 0,
lazyLoading = false
}) => {
const showSkeleton = skeletonColumnCount > 0
return (
<div className='objectKanbanHeader'>
<div className='objectKanbanHeaderTrack' ref={trackRef}>
<Flex gap='middle' className='objectKanbanHeaderRow'>
<Flex gap='middle' className='objectKanbanHeaderRow' align='center'>
{showSkeleton
? Array.from({ length: skeletonColumnCount }).map((_, index) => (
<div
@ -32,7 +34,13 @@ const ObjectKanbanHeader = ({
: categoryValues.map((categoryValue) => {
const columnKey = getCategoryValueKey(categoryValue)
return (
<div key={columnKey} className='objectKanbanHeaderCell'>
<Flex
key={columnKey}
className='objectKanbanHeaderCell'
align='center'
justify='space-between'
gap={8}
>
{categoryPropertyDef ? (
<ObjectProperty
{...categoryPropertyDef}
@ -41,7 +49,13 @@ const ObjectKanbanHeader = ({
name={categoryProperty}
/>
) : null}
</div>
{lazyLoading && (
<LoadingOutlined
spin
style={{ flexShrink: 0, marginRight: 6 }}
/>
)}
</Flex>
)
})}
</Flex>
@ -57,7 +71,8 @@ ObjectKanbanHeader.propTypes = {
categoryProperty: PropTypes.string.isRequired,
categoryPropertyDef: PropTypes.object,
trackRef: PropTypes.object,
skeletonColumnCount: PropTypes.number
skeletonColumnCount: PropTypes.number,
lazyLoading: PropTypes.bool
}
export default ObjectKanbanHeader

View File

@ -60,6 +60,7 @@ import Tooltip from './Tooltip'
import { ObjectTableFilterContext } from './ObjectTableFilterContext'
import ObjectListViewContext from '../context/ObjectListViewContext'
import { isCardsView, isKanbanView, normalizeViewMode } from './viewModeUtils'
import LoadingPlaceholder from './LoadingPlaceholder'
const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel)
@ -563,7 +564,7 @@ const ObjectTable = forwardRef(
)
}, [])
const renderActions = (objectData) => {
const renderActions = (objectData, actionsDisabled = false) => {
return (
<Flex gap='small' align='center' justify='center'>
{rowActions.map((action, index) => {
@ -576,9 +577,10 @@ const ObjectTable = forwardRef(
action.disabled({
...objectData,
_user: userProfile
})
}) ||
actionsDisabled
} else {
disabled = denied || action.disabled
disabled = denied || action.disabled || actionsDisabled
}
}
return (
@ -1175,8 +1177,10 @@ const ObjectTable = forwardRef(
if (!silent) {
setLoading(true)
setLazyLoading(false)
} else {
setLoading(false)
setLazyLoading(true)
}
try {
let loadKanban = kanbanRef.current?.load
@ -1191,8 +1195,12 @@ const ObjectTable = forwardRef(
} catch (error) {
logger.error('Error loading kanban view:', error)
} finally {
if (!silent && generation === dataLoadGenerationRef.current) {
setLoading(false)
if (generation === dataLoadGenerationRef.current) {
if (!silent) {
setLoading(false)
} else {
setLazyLoading(false)
}
}
}
return
@ -1204,6 +1212,7 @@ const ObjectTable = forwardRef(
loadingPagesRef.current.clear()
pendingScrollAnchorRef.current = null
setLoading(false)
setLazyLoading(true)
try {
const firstResult = await fetchPage(pageNum, filter, sorter)
@ -1224,6 +1233,10 @@ const ObjectTable = forwardRef(
if (!isStaleDataLoad(generation)) {
logger.error(`Error loading page ${pageNum}:`, error)
}
} finally {
if (!isStaleDataLoad(generation)) {
setLazyLoading(false)
}
}
return
}
@ -1235,6 +1248,7 @@ const ObjectTable = forwardRef(
const skeletonPage = createSkeletonPage(pageNum)
setTablePages([skeletonPage])
setLoading(true)
setLazyLoading(false)
if (isStaleDataLoad(generation)) return
try {
@ -1970,6 +1984,7 @@ const ObjectTable = forwardRef(
modelProperties={modelProperties}
visibleColumns={visibleColumns}
record={record}
lazyLoading={lazyLoading}
isEditing={isEditing}
rowActions={rowActions}
renderActions={renderActions}
@ -1981,6 +1996,14 @@ const ObjectTable = forwardRef(
)
})}
</Row>
{lazyLoading && tableData.length <= 0 && (
<LoadingPlaceholder
message='Loading, please wait...'
hasBackground={true}
hasBorder={false}
height='260px'
/>
)}
</div>
</ScrollBox>
)
@ -2030,6 +2053,7 @@ const ObjectTable = forwardRef(
baseFilter={effectiveFilter}
masterFilter={resolvedMasterFilter}
sorter={effectiveSorter}
lazyLoading={lazyLoading}
pageSize={pageSize}
model={model}
modelProperties={modelProperties}