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 { Text } = Typography
const LoadingPlaceholder = ({ message, hasBackground = true }) => { const LoadingPlaceholder = ({
message,
hasBackground = true,
hasBorder = true,
height = undefined
}) => {
return ( return (
<Card <Card
size='small' size='small'
style={{ style={{
background: hasBackground == false ? 'transparent' : undefined, 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 <Flex
@ -32,7 +48,9 @@ const LoadingPlaceholder = ({ message, hasBackground = true }) => {
LoadingPlaceholder.propTypes = { LoadingPlaceholder.propTypes = {
message: PropTypes.string.isRequired, message: PropTypes.string.isRequired,
hasBackground: PropTypes.bool hasBackground: PropTypes.bool,
hasBorder: PropTypes.bool,
height: PropTypes.string
} }
export default LoadingPlaceholder export default LoadingPlaceholder

View File

@ -3,6 +3,7 @@ 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'
import { LoadingOutlined } from '@ant-design/icons'
const ObjectCard = ({ const ObjectCard = ({
isSkeleton = false, isSkeleton = false,
@ -13,6 +14,7 @@ const ObjectCard = ({
isEditing = false, isEditing = false,
rowActions = [], rowActions = [],
renderActions, renderActions,
lazyLoading = false,
cardStyle = 'borderless' cardStyle = 'borderless'
}) => { }) => {
const descriptionItems = [] const descriptionItems = []
@ -59,7 +61,7 @@ const ObjectCard = ({
var actions = undefined var actions = undefined
if (rowActions.length > 0) { if (rowActions.length > 0) {
actions = renderActions(record) actions = renderActions(record, lazyLoading)
} }
const primaryDescriptionItems = thumbnailPresent const primaryDescriptionItems = thumbnailPresent
@ -146,8 +148,20 @@ const ObjectCard = ({
{actions && ( {actions && (
<> <>
<Divider style={{ margin: '4px 0 0 0' }} /> <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> </Flex>
</> </>
)} )}
@ -166,7 +180,8 @@ ObjectCard.propTypes = {
rowActions: PropTypes.array, rowActions: PropTypes.array,
renderActions: PropTypes.func.isRequired, renderActions: PropTypes.func.isRequired,
cardStyle: PropTypes.string, cardStyle: PropTypes.string,
isSkeleton: PropTypes.bool isSkeleton: PropTypes.bool,
lazyLoading: PropTypes.bool
} }
export default ObjectCard export default ObjectCard

View File

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

View File

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

View File

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

View File

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