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:
parent
1466fe0f62
commit
dc237eecdd
@ -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 {
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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) && (
|
||||||
|
|||||||
77
src/components/Dashboard/common/Spin.jsx
Normal file
77
src/components/Dashboard/common/Spin.jsx
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user