Compare commits

..

2 Commits

Author SHA1 Message Date
cb49baaf93 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.
2026-09-03 03:53:31 +01:00
dc237eecdd Implement Custom Spin Component and Update Loading States
- Introduced a new Spin component to standardize loading indicators across the application, enhancing visual consistency.
- Updated ObjectKanban, ObjectKanbanColumn, and ObjectTable components to utilize the new Spin component, simplifying loading state management.
- Refactored CSS styles in App.css to support the new Spin component, including responsive design adjustments for various loading sizes.
- Removed redundant loading indicators and streamlined loading behavior for improved user experience during data fetching.
2026-09-03 03:24:25 +01:00
8 changed files with 317 additions and 152 deletions

View File

@ -475,6 +475,67 @@ body {
.ant-spin-blur { .ant-spin-blur {
filter: blur(3px); filter: blur(3px);
} }
.spin {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--color-primary);
font-size: 20px;
line-height: 1;
}
.spin-nested {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
flex: 1;
}
.spin-content {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
flex: 1;
transition: opacity 0.3s;
}
.spin-content.is-spinning {
filter: blur(3px);
opacity: 0.5;
user-select: none;
pointer-events: none;
}
.spin-overlay {
position: absolute;
inset: 0;
z-index: 4;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-primary);
font-size: 20px;
pointer-events: none;
}
.spin.spin-sm,
.spin-nested.spin-sm .spin-overlay {
font-size: 14px;
}
.spin.spin-lg,
.spin-nested.spin-lg .spin-overlay {
font-size: 32px;
}
/* --- End of src/index.css --- */ /* --- End of src/index.css --- */
/* --- Start of src/components/Dashboard/Layout.css --- */ /* --- Start of src/components/Dashboard/Layout.css --- */
@ -1377,26 +1438,6 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
flex-direction: column; flex-direction: column;
} }
.objectTableCardsContainer > .ant-spin-nested-loading,
.objectTableCardsContainer > .ant-spin-nested-loading > .ant-spin-container,
.objectTableCardsContainer .ant-spin-nested-loading {
width: 100%;
height: 100%;
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
}
.objectTableCardsContainer .ant-spin-container {
width: 100%;
height: 100%;
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
}
.objectKanbanContainerWrapper { .objectKanbanContainerWrapper {
position: relative; position: relative;
width: 100%; width: 100%;
@ -1407,15 +1448,6 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
flex-direction: column; flex-direction: column;
} }
.objectKanbanContainerWrapper > .ant-spin-nested-loading,
.objectKanbanContainerWrapper > .ant-spin-nested-loading > .ant-spin-container {
height: 100%;
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
}
.objectKanbanContainerSkeletons { .objectKanbanContainerSkeletons {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1449,12 +1481,8 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
height: 100%; height: 100%;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
} display: flex;
flex-direction: column;
.objectKanbanWrap > .ant-spin-nested-loading,
.objectKanbanWrap > .ant-spin-nested-loading > .ant-spin-container {
height: 100%;
min-height: 0;
} }
.objectKanbanScroll { .objectKanbanScroll {

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,9 +148,21 @@ const ObjectCard = ({
{actions && ( {actions && (
<> <>
<Divider style={{ margin: '4px 0 0 0' }} /> <Divider style={{ margin: '4px 0 0 0' }} />
<Flex align='flex-end' gap={10}>
<Flex
align='center'
gap={10}
style={{ width: '100%' }}
justify='space-between'
>
<Flex align='center' gap={10}>
{actions} {actions}
</Flex> </Flex>
{lazyLoading && (
<LoadingOutlined spin style={{ marginRight: 4 }} />
)}
</Flex>
</> </>
)} )}
</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

@ -8,14 +8,14 @@ import {
useRef, useRef,
useState useState
} from 'react' } from 'react'
import { Empty, Flex, Spin } from 'antd' import { Empty, Flex } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import ObjectCard from './ObjectCard' import ObjectCard from './ObjectCard'
import ObjectKanbanColumn from './ObjectKanbanColumn' import ObjectKanbanColumn from './ObjectKanbanColumn'
import ObjectKanbanHeader from './ObjectKanbanHeader' import ObjectKanbanHeader from './ObjectKanbanHeader'
import ScrollBox from './ScrollBox' import ScrollBox from './ScrollBox'
import Spin from './Spin'
import { getCategoryValueKey } from './viewModeUtils' import { getCategoryValueKey } from './viewModeUtils'
const KANBAN_COLUMN_WIDTH = 360 const KANBAN_COLUMN_WIDTH = 360
@ -101,6 +101,7 @@ const ObjectKanban = forwardRef(
baseFilter = {}, baseFilter = {},
masterFilter = {}, masterFilter = {},
sorter = {}, sorter = {},
lazyLoading = false,
pageSize = 25, pageSize = 25,
model, model,
modelProperties, modelProperties,
@ -311,11 +312,7 @@ const ObjectKanban = forwardRef(
return ( return (
<div className='objectKanbanContainerWrapper'> <div className='objectKanbanContainerWrapper'>
<Spin <Spin spinning={showSkeleton}>
indicator={<LoadingOutlined />}
spinning={showSkeleton}
style={{ height: '100%', flex: 1, minHeight: 0 }}
>
<div className='objectKanbanContainerSkeletons'> <div className='objectKanbanContainerSkeletons'>
<ObjectKanbanHeader <ObjectKanbanHeader
categoryValues={categoryValues} categoryValues={categoryValues}
@ -325,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 && (
@ -414,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

@ -9,11 +9,11 @@ import {
useRef, useRef,
useState useState
} from 'react' } from 'react'
import { Flex, Spin } from 'antd' import { Flex } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import ObjectCard from './ObjectCard' import ObjectCard from './ObjectCard'
import Spin from './Spin'
import { toCategoryFilterValue } from './viewModeUtils' import { toCategoryFilterValue } from './viewModeUtils'
const SCROLL_THRESHOLD = 50 const SCROLL_THRESHOLD = 50
@ -33,6 +33,7 @@ const ObjectKanbanColumn = forwardRef(
visibleColumns = {}, visibleColumns = {},
isEditing = false, isEditing = false,
rowActions = [], rowActions = [],
lazyLoading = false,
renderActions, renderActions,
scrollElement = null scrollElement = null
}, },
@ -358,7 +359,8 @@ const ObjectKanbanColumn = forwardRef(
[loadBoundaryPage] [loadBoundaryPage]
) )
const loadInitialPage = useCallback(async ({ silent = false } = {}) => { const loadInitialPage = useCallback(
async ({ silent = false } = {}) => {
dataLoadGenerationRef.current += 1 dataLoadGenerationRef.current += 1
loadingPagesRef.current.clear() loadingPagesRef.current.clear()
pendingScrollAnchorRef.current = null pendingScrollAnchorRef.current = null
@ -403,7 +405,8 @@ const ObjectKanbanColumn = forwardRef(
setLoading(false) setLoading(false)
} }
} }
}, [ },
[
categoryProperty, categoryProperty,
categoryValue, categoryValue,
createPageWindow, createPageWindow,
@ -411,7 +414,8 @@ const ObjectKanbanColumn = forwardRef(
fetchPage, fetchPage,
isStaleDataLoad, isStaleDataLoad,
setTablePages setTablePages
]) ]
)
const reloadLoadedPages = useCallback(async () => { const reloadLoadedPages = useCallback(async () => {
const loadedPages = pagesRef.current.filter( const loadedPages = pagesRef.current.filter(
@ -501,7 +505,7 @@ const ObjectKanbanColumn = forwardRef(
return ( return (
<Flex vertical className='objectKanbanColumn'> <Flex vertical className='objectKanbanColumn'>
<div className='objectKanbanColumnCards' ref={containerRef}> <div className='objectKanbanColumnCards' ref={containerRef}>
<Spin indicator={<LoadingOutlined />} spinning={loading}> <Spin spinning={loading}>
<Flex <Flex
vertical vertical
gap='middle' gap='middle'
@ -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

@ -15,7 +15,6 @@ import {
Row, Row,
Col, Col,
Flex, Flex,
Spin,
Button, Button,
Space, Space,
Form, Form,
@ -47,6 +46,7 @@ import { useActions } from '../context/ActionsContext'
import ActionsIcon from '../../Icons/ActionsIcon' import ActionsIcon from '../../Icons/ActionsIcon'
import FilterIcon from '../../Icons/FilterIcon' import FilterIcon from '../../Icons/FilterIcon'
import ScrollBox from './ScrollBox' import ScrollBox from './ScrollBox'
import Spin from './Spin'
import SimplePropertyFilter from './SimplePropertyFilter' import SimplePropertyFilter from './SimplePropertyFilter'
import QuickPropertyFilters from './QuickPropertyFilters' import QuickPropertyFilters from './QuickPropertyFilters'
import FilterInput from './FilterInput' import FilterInput from './FilterInput'
@ -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) {
if (!silent) {
setLoading(false) 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>
) )
@ -2022,11 +2045,7 @@ const ObjectTable = forwardRef(
<Splitter.Panel> <Splitter.Panel>
{isKanban ? ( {isKanban ? (
<div className='objectKanbanWrap'> <div className='objectKanbanWrap'>
<Spin <Spin spinning={loading}>
indicator={<LoadingOutlined />}
spinning={loading}
style={{ height: '100%' }}
>
<ObjectKanban <ObjectKanban
ref={kanbanRef} ref={kanbanRef}
type={type} type={type}
@ -2034,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}
@ -2049,20 +2069,10 @@ const ObjectTable = forwardRef(
className='objectTableCardsContainer' className='objectTableCardsContainer'
ref={objectTableCardsContainerRef} ref={objectTableCardsContainerRef}
> >
<Spin <Spin spinning={loading}>{renderCards()}</Spin>
indicator={<LoadingOutlined />}
spinning={loading}
style={{
height: '300px',
flex: 1,
minHeight: 0,
width: '100%'
}}
>
{renderCards()}
</Spin>
</div> </div>
) : ( ) : (
<Spin spinning={loading}>
<Table <Table
key={tableListKey} key={tableListKey}
ref={tableRef} ref={tableRef}
@ -2072,10 +2082,6 @@ const ObjectTable = forwardRef(
pagination={false} pagination={false}
scroll={{ y: adjustedScrollHeight }} scroll={{ y: adjustedScrollHeight }}
rowKey='_id' rowKey='_id'
loading={{
spinning: loading,
indicator: <LoadingOutlined spin />
}}
onScroll={handleScroll} onScroll={handleScroll}
onChange={handleTableChange} onChange={handleTableChange}
showSorterTooltip={false} showSorterTooltip={false}
@ -2084,6 +2090,7 @@ const ObjectTable = forwardRef(
components={components} components={components}
onRow={onRow} onRow={onRow}
/> />
</Spin>
)} )}
</Splitter.Panel> </Splitter.Panel>
{(showSortSidebar || showFilterSidebar) && ( {(showSortSidebar || showFilterSidebar) && (

View File

@ -0,0 +1,77 @@
import PropTypes from 'prop-types'
import { LoadingOutlined } from '@ant-design/icons'
const SIZE_CLASS = {
small: 'spin-sm',
default: '',
large: 'spin-lg'
}
const DEFAULT_INDICATOR = (
<LoadingOutlined spin style={{ color: 'var(--color-primary)' }} />
)
const Spin = ({
spinning = true,
children,
indicator = DEFAULT_INDICATOR,
size = 'default',
style,
className
}) => {
const sizeClass = SIZE_CLASS[size] ?? ''
if (children == null) {
if (!spinning) return null
return (
<div
className={['spin', sizeClass, className].filter(Boolean).join(' ')}
style={style}
role='status'
aria-live='polite'
>
{indicator}
</div>
)
}
return (
<div
className={[
'spin-nested',
spinning ? 'is-spinning' : null,
sizeClass,
className
]
.filter(Boolean)
.join(' ')}
style={style}
>
{spinning && (
<div className='spin-overlay' role='status' aria-live='polite'>
{indicator}
</div>
)}
<div
className={['spin-content', spinning ? 'is-spinning' : null]
.filter(Boolean)
.join(' ')}
aria-busy={spinning}
>
{children}
</div>
</div>
)
}
Spin.propTypes = {
spinning: PropTypes.bool,
children: PropTypes.node,
indicator: PropTypes.node,
size: PropTypes.oneOf(['small', 'default', 'large']),
style: PropTypes.object,
className: PropTypes.string
}
export default Spin