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.
This commit is contained in:
Tom Butcher 2026-09-03 03:24:25 +01:00
parent 1466fe0f62
commit dc237eecdd
5 changed files with 168 additions and 84 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

@ -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
@ -311,11 +311,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}

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
@ -501,7 +501,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'

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'
@ -2022,11 +2022,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}
@ -2049,41 +2045,28 @@ 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>
) : ( ) : (
<Table <Spin spinning={loading}>
key={tableListKey} <Table
ref={tableRef} key={tableListKey}
dataSource={tableData} ref={tableRef}
columns={columnsWithSkeleton} dataSource={tableData}
className='dashboard-table' columns={columnsWithSkeleton}
pagination={false} className='dashboard-table'
scroll={{ y: adjustedScrollHeight }} pagination={false}
rowKey='_id' scroll={{ y: adjustedScrollHeight }}
loading={{ rowKey='_id'
spinning: loading, onScroll={handleScroll}
indicator: <LoadingOutlined spin /> onChange={handleTableChange}
}} showSorterTooltip={false}
onScroll={handleScroll} style={{ height: '100%' }}
onChange={handleTableChange} size={size}
showSorterTooltip={false} components={components}
style={{ height: '100%' }} onRow={onRow}
size={size} />
components={components} </Spin>
onRow={onRow}
/>
)} )}
</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