Enhance ObjectKanban with Skeleton Loading and Responsive Layout
- Introduced a skeleton loading state in the ObjectKanban component to improve user experience during data fetching. - Updated CSS styles for the ObjectKanban and related components to ensure a responsive layout, accommodating various screen sizes. - Refactored ObjectKanbanHeader to conditionally render skeletons based on loading state, enhancing visual feedback. - Improved filter and sorter handling in ObjectKanbanColumn for better data management during loading.
This commit is contained in:
parent
0eae922a9e
commit
26d8c80d54
@ -1372,16 +1372,29 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
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 {
|
||||
@ -1394,6 +1407,15 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -8,14 +8,91 @@ import {
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import { Empty, Flex } from 'antd'
|
||||
import { Empty, Flex, Spin } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import ObjectCard from './ObjectCard'
|
||||
import ObjectKanbanColumn from './ObjectKanbanColumn'
|
||||
import ObjectKanbanHeader from './ObjectKanbanHeader'
|
||||
import ScrollBox from './ScrollBox'
|
||||
import { getCategoryValueKey } from './viewModeUtils'
|
||||
|
||||
const KANBAN_COLUMN_WIDTH = 360
|
||||
const KANBAN_GAP = 16
|
||||
const KANBAN_CARD_MIN_HEIGHT = 136
|
||||
|
||||
const getKanbanSkeletonLayout = (width, height) => {
|
||||
const columnCount =
|
||||
Math.max(
|
||||
1,
|
||||
Math.floor((width + KANBAN_GAP) / (KANBAN_COLUMN_WIDTH + KANBAN_GAP))
|
||||
) + 1
|
||||
const cardCount = Math.max(
|
||||
1,
|
||||
Math.floor((height - 32) / KANBAN_CARD_MIN_HEIGHT)
|
||||
)
|
||||
|
||||
return { columnCount, cardCount }
|
||||
}
|
||||
|
||||
const ObjectKanbanSkeletonColumns = ({
|
||||
columnCount,
|
||||
cardCount = 0,
|
||||
trackRef,
|
||||
model,
|
||||
modelProperties,
|
||||
visibleColumns,
|
||||
keyPrefix = 'skeleton'
|
||||
}) => (
|
||||
<div className='objectKanbanSkeletonColumns objectKanbanSkeletonColumns--loading'>
|
||||
<div className='objectKanbanSkeletonTrack' ref={trackRef}>
|
||||
<Flex gap='middle' className='objectKanbanSkeletonRow'>
|
||||
{Array.from({ length: columnCount }).map((_, columnIndex) => (
|
||||
<div
|
||||
key={`${keyPrefix}-${columnIndex}`}
|
||||
className='objectKanbanColumnSkeleton'
|
||||
>
|
||||
<div className='objectKanbanColumnCards'>
|
||||
{cardCount > 0 && (
|
||||
<Flex
|
||||
vertical
|
||||
gap='middle'
|
||||
className='objectKanbanColumnCardsInner'
|
||||
>
|
||||
{Array.from({ length: cardCount }).map((_, cardIndex) => (
|
||||
<ObjectCard
|
||||
key={cardIndex}
|
||||
isSkeleton
|
||||
model={model}
|
||||
modelProperties={modelProperties}
|
||||
visibleColumns={visibleColumns}
|
||||
record={{
|
||||
_id: `${keyPrefix}-${columnIndex}-${cardIndex}`
|
||||
}}
|
||||
cardStyle='bordered'
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
ObjectKanbanSkeletonColumns.propTypes = {
|
||||
columnCount: PropTypes.number.isRequired,
|
||||
cardCount: PropTypes.number,
|
||||
trackRef: PropTypes.object,
|
||||
model: PropTypes.object.isRequired,
|
||||
modelProperties: PropTypes.array.isRequired,
|
||||
visibleColumns: PropTypes.object,
|
||||
keyPrefix: PropTypes.string
|
||||
}
|
||||
|
||||
const ObjectKanban = forwardRef(
|
||||
(
|
||||
{
|
||||
@ -38,15 +115,24 @@ const ObjectKanban = forwardRef(
|
||||
const columnRefs = useRef({})
|
||||
const headerTrackRef = useRef(null)
|
||||
const skeletonTrackRef = useRef(null)
|
||||
const kanbanBodyRef = useRef(null)
|
||||
const [scrollElement, setScrollElement] = useState(null)
|
||||
const [categoryValues, setCategoryValues] = useState([])
|
||||
const [hasLoaded, setHasLoaded] = useState(false)
|
||||
const [loadedFilter, setLoadedFilter] = useState(baseFilter)
|
||||
const [loadedSorter, setLoadedSorter] = useState(sorter)
|
||||
const [skeletonLayout, setSkeletonLayout] = useState({
|
||||
columnCount: 3,
|
||||
cardCount: 3
|
||||
})
|
||||
|
||||
const baseFilterRef = useRef(baseFilter)
|
||||
const masterFilterRef = useRef(masterFilter)
|
||||
const sorterRef = useRef(sorter)
|
||||
const getModelPropertyValuesRef = useRef(getModelPropertyValues)
|
||||
const lastCategoryQueryKeyRef = useRef(null)
|
||||
const hasLoadedRef = useRef(false)
|
||||
const loadGenerationRef = useRef(0)
|
||||
|
||||
baseFilterRef.current = baseFilter
|
||||
masterFilterRef.current = masterFilter
|
||||
@ -57,6 +143,23 @@ const ObjectKanban = forwardRef(
|
||||
setScrollElement(node)
|
||||
}, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const kanbanBody = kanbanBodyRef.current
|
||||
if (!kanbanBody) return undefined
|
||||
|
||||
const updateLayout = () => {
|
||||
const { width, height } = kanbanBody.getBoundingClientRect()
|
||||
setSkeletonLayout(getKanbanSkeletonLayout(width, height))
|
||||
}
|
||||
|
||||
updateLayout()
|
||||
|
||||
const observer = new ResizeObserver(updateLayout)
|
||||
observer.observe(kanbanBody)
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [categoryProperty])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const mainScroll = scrollElement
|
||||
const headerTrack = headerTrackRef.current
|
||||
@ -109,8 +212,11 @@ const ObjectKanban = forwardRef(
|
||||
|
||||
const load = useCallback(
|
||||
async (filter = null, sorterArg = null) => {
|
||||
const generation = ++loadGenerationRef.current
|
||||
|
||||
if (!categoryProperty) {
|
||||
lastCategoryQueryKeyRef.current = null
|
||||
hasLoadedRef.current = true
|
||||
setCategoryValues([])
|
||||
setHasLoaded(true)
|
||||
return
|
||||
@ -118,14 +224,21 @@ const ObjectKanban = forwardRef(
|
||||
|
||||
const activeFilter = filter ?? baseFilterRef.current
|
||||
const activeSorter = sorterArg ?? sorterRef.current
|
||||
const activeMasterFilter = masterFilterRef.current
|
||||
const queryKey = buildCategoryQueryKey(activeFilter, activeSorter)
|
||||
|
||||
if (lastCategoryQueryKeyRef.current === queryKey) {
|
||||
const canReuseColumns =
|
||||
lastCategoryQueryKeyRef.current === queryKey && hasLoadedRef.current
|
||||
|
||||
if (canReuseColumns) {
|
||||
await reloadColumns()
|
||||
if (generation !== loadGenerationRef.current) return
|
||||
hasLoadedRef.current = true
|
||||
setHasLoaded(true)
|
||||
return
|
||||
}
|
||||
|
||||
hasLoadedRef.current = false
|
||||
setHasLoaded(false)
|
||||
|
||||
try {
|
||||
@ -134,19 +247,25 @@ const ObjectKanban = forwardRef(
|
||||
categoryProperty,
|
||||
{
|
||||
filter: activeFilter,
|
||||
masterFilter: masterFilterRef.current
|
||||
masterFilter: activeMasterFilter
|
||||
}
|
||||
)
|
||||
if (generation !== loadGenerationRef.current) return
|
||||
const nextValues = Array.isArray(values) ? values : []
|
||||
setCategoryValues(nextValues)
|
||||
lastCategoryQueryKeyRef.current = queryKey
|
||||
} catch (error) {
|
||||
console.error('Error fetching kanban category values:', error)
|
||||
setCategoryValues([])
|
||||
lastCategoryQueryKeyRef.current = null
|
||||
throw error
|
||||
} finally {
|
||||
hasLoadedRef.current = true
|
||||
setLoadedFilter(activeFilter)
|
||||
setLoadedSorter(activeSorter)
|
||||
setCategoryValues(nextValues)
|
||||
setHasLoaded(true)
|
||||
} catch (error) {
|
||||
if (generation !== loadGenerationRef.current) return
|
||||
console.error('Error fetching kanban category values:', error)
|
||||
lastCategoryQueryKeyRef.current = null
|
||||
hasLoadedRef.current = true
|
||||
setCategoryValues([])
|
||||
setHasLoaded(true)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[buildCategoryQueryKey, categoryProperty, reloadColumns, type]
|
||||
@ -154,6 +273,7 @@ const ObjectKanban = forwardRef(
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
lastCategoryQueryKeyRef.current = null
|
||||
hasLoadedRef.current = false
|
||||
await load(baseFilterRef.current, sorterRef.current)
|
||||
}, [load])
|
||||
|
||||
@ -182,82 +302,100 @@ const ObjectKanban = forwardRef(
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasLoaded || categoryValues.length === 0) {
|
||||
return (
|
||||
<div className='objectKanbanContainer' style={{ height: '100%' }} />
|
||||
)
|
||||
}
|
||||
const showSkeleton = !hasLoaded
|
||||
|
||||
return (
|
||||
<div className='objectKanbanContainerWrapper'>
|
||||
<div className='objectKanbanContainerSkeletons'>
|
||||
<ObjectKanbanHeader
|
||||
categoryValues={categoryValues}
|
||||
categoryProperty={categoryProperty}
|
||||
categoryPropertyDef={categoryPropertyDef}
|
||||
trackRef={headerTrackRef}
|
||||
/>
|
||||
<div className='objectKanbanKanbanBody'>
|
||||
<div className='objectKanbanContainer'>
|
||||
<ScrollBox
|
||||
className='objectKanbanScroll'
|
||||
scrollableNodeProps={{ ref: setScrollContainerRef }}
|
||||
>
|
||||
<Flex gap='middle' className='objectKanban'>
|
||||
{categoryValues.map((categoryValue) => {
|
||||
const columnKey = getCategoryValueKey(categoryValue)
|
||||
return (
|
||||
<ObjectKanbanColumn
|
||||
key={columnKey}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
columnRefs.current[columnKey] = node
|
||||
} else {
|
||||
delete columnRefs.current[columnKey]
|
||||
}
|
||||
}}
|
||||
scrollElement={scrollElement}
|
||||
type={type}
|
||||
categoryProperty={categoryProperty}
|
||||
categoryValue={categoryValue}
|
||||
baseFilter={baseFilter}
|
||||
masterFilter={masterFilter}
|
||||
sorter={sorter}
|
||||
pageSize={pageSize}
|
||||
model={model}
|
||||
modelProperties={modelProperties}
|
||||
visibleColumns={visibleColumns}
|
||||
isEditing={isEditing}
|
||||
rowActions={rowActions}
|
||||
renderActions={renderActions}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
</ScrollBox>
|
||||
</div>
|
||||
<div className='objectKanbanSkeletonColumns'>
|
||||
<div
|
||||
className='objectKanbanSkeletonTrack'
|
||||
ref={skeletonTrackRef}
|
||||
>
|
||||
<Flex gap='middle' className='objectKanbanSkeletonRow'>
|
||||
{categoryValues.map((categoryValue) => {
|
||||
const columnKey = getCategoryValueKey(categoryValue)
|
||||
return (
|
||||
<div
|
||||
key={columnKey}
|
||||
className='objectKanbanColumnSkeleton'
|
||||
>
|
||||
<div className='objectKanbanColumnCards' />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
</div>
|
||||
<Spin
|
||||
indicator={<LoadingOutlined />}
|
||||
spinning={showSkeleton}
|
||||
style={{ height: '100%', flex: 1, minHeight: 0 }}
|
||||
>
|
||||
<div className='objectKanbanContainerSkeletons'>
|
||||
<ObjectKanbanHeader
|
||||
categoryValues={categoryValues}
|
||||
categoryProperty={categoryProperty}
|
||||
categoryPropertyDef={categoryPropertyDef}
|
||||
trackRef={headerTrackRef}
|
||||
skeletonColumnCount={
|
||||
showSkeleton ? skeletonLayout.columnCount : 0
|
||||
}
|
||||
/>
|
||||
<div className='objectKanbanKanbanBody' ref={kanbanBodyRef}>
|
||||
{!showSkeleton && (
|
||||
<div className='objectKanbanContainer'>
|
||||
<ScrollBox
|
||||
className='objectKanbanScroll'
|
||||
scrollableNodeProps={{ ref: setScrollContainerRef }}
|
||||
>
|
||||
<Flex gap='middle' className='objectKanban'>
|
||||
{categoryValues.map((categoryValue) => {
|
||||
const columnKey = getCategoryValueKey(categoryValue)
|
||||
return (
|
||||
<ObjectKanbanColumn
|
||||
key={columnKey}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
columnRefs.current[columnKey] = node
|
||||
} else {
|
||||
delete columnRefs.current[columnKey]
|
||||
}
|
||||
}}
|
||||
scrollElement={scrollElement}
|
||||
type={type}
|
||||
categoryProperty={categoryProperty}
|
||||
categoryValue={categoryValue}
|
||||
baseFilter={loadedFilter}
|
||||
masterFilter={masterFilter}
|
||||
sorter={loadedSorter}
|
||||
pageSize={pageSize}
|
||||
model={model}
|
||||
modelProperties={modelProperties}
|
||||
visibleColumns={visibleColumns}
|
||||
isEditing={isEditing}
|
||||
rowActions={rowActions}
|
||||
renderActions={renderActions}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
</ScrollBox>
|
||||
</div>
|
||||
)}
|
||||
{showSkeleton ? (
|
||||
<ObjectKanbanSkeletonColumns
|
||||
columnCount={skeletonLayout.columnCount}
|
||||
cardCount={skeletonLayout.cardCount}
|
||||
trackRef={skeletonTrackRef}
|
||||
model={model}
|
||||
modelProperties={modelProperties}
|
||||
visibleColumns={visibleColumns}
|
||||
/>
|
||||
) : (
|
||||
<div className='objectKanbanSkeletonColumns'>
|
||||
<div
|
||||
className='objectKanbanSkeletonTrack'
|
||||
ref={skeletonTrackRef}
|
||||
>
|
||||
<Flex gap='middle' className='objectKanbanSkeletonRow'>
|
||||
{categoryValues.map((categoryValue) => {
|
||||
const columnKey = getCategoryValueKey(categoryValue)
|
||||
return (
|
||||
<div
|
||||
key={columnKey}
|
||||
className='objectKanbanColumnSkeleton'
|
||||
>
|
||||
<div className='objectKanbanColumnCards' />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -164,17 +164,18 @@ const ObjectKanbanColumn = forwardRef(
|
||||
)
|
||||
|
||||
const fetchPage = useCallback(
|
||||
async (pageNum) => {
|
||||
const filter = {
|
||||
async (pageNum, overrides = {}) => {
|
||||
const filter = overrides.filter ?? {
|
||||
...masterFilterRef.current,
|
||||
...baseFilterRef.current,
|
||||
[categoryProperty]: toCategoryFilterValue(categoryValue)
|
||||
}
|
||||
const sorter = overrides.sorter ?? sorterRef.current
|
||||
return fetchObjectsRef.current(type, {
|
||||
page: pageNum,
|
||||
limit: pageSize,
|
||||
filter,
|
||||
sorter: sorterRef.current
|
||||
sorter
|
||||
})
|
||||
},
|
||||
[categoryProperty, categoryValue, pageSize, type]
|
||||
@ -362,6 +363,12 @@ const ObjectKanbanColumn = forwardRef(
|
||||
loadingPagesRef.current.clear()
|
||||
pendingScrollAnchorRef.current = null
|
||||
const generation = dataLoadGenerationRef.current
|
||||
const filter = {
|
||||
...masterFilterRef.current,
|
||||
...baseFilterRef.current,
|
||||
[categoryProperty]: toCategoryFilterValue(categoryValue)
|
||||
}
|
||||
const sorter = sorterRef.current
|
||||
|
||||
pagesRef.current = []
|
||||
setPages([])
|
||||
@ -371,7 +378,7 @@ const ObjectKanbanColumn = forwardRef(
|
||||
setTablePages([skeletonPage])
|
||||
|
||||
try {
|
||||
const firstResult = await fetchPage(1)
|
||||
const firstResult = await fetchPage(1, { filter, sorter })
|
||||
if (isStaleDataLoad(generation)) return
|
||||
const loadedPages = [
|
||||
{
|
||||
@ -393,6 +400,8 @@ const ObjectKanbanColumn = forwardRef(
|
||||
}
|
||||
}
|
||||
}, [
|
||||
categoryProperty,
|
||||
categoryValue,
|
||||
createPageWindow,
|
||||
createSkeletonPage,
|
||||
fetchPage,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Flex } from 'antd'
|
||||
import { Flex, Skeleton } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import ObjectProperty from './ObjectProperty'
|
||||
import { getCategoryValueKey } from './viewModeUtils'
|
||||
@ -7,27 +7,43 @@ const ObjectKanbanHeader = ({
|
||||
categoryValues,
|
||||
categoryProperty,
|
||||
categoryPropertyDef,
|
||||
trackRef
|
||||
trackRef,
|
||||
skeletonColumnCount = 0
|
||||
}) => {
|
||||
const showSkeleton = skeletonColumnCount > 0
|
||||
|
||||
return (
|
||||
<div className='objectKanbanHeader'>
|
||||
<div className='objectKanbanHeaderTrack' ref={trackRef}>
|
||||
<Flex gap='middle' className='objectKanbanHeaderRow'>
|
||||
{categoryValues.map((categoryValue) => {
|
||||
const columnKey = getCategoryValueKey(categoryValue)
|
||||
return (
|
||||
<div key={columnKey} className='objectKanbanHeaderCell'>
|
||||
{categoryPropertyDef ? (
|
||||
<ObjectProperty
|
||||
{...categoryPropertyDef}
|
||||
value={categoryValue}
|
||||
objectData={{ [categoryProperty]: categoryValue }}
|
||||
name={categoryProperty}
|
||||
{showSkeleton
|
||||
? Array.from({ length: skeletonColumnCount }).map((_, index) => (
|
||||
<div
|
||||
key={`skeleton-header-${index}`}
|
||||
className='objectKanbanHeaderCell'
|
||||
>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size='small'
|
||||
style={{ width: '60%', height: 24 }}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))
|
||||
: categoryValues.map((categoryValue) => {
|
||||
const columnKey = getCategoryValueKey(categoryValue)
|
||||
return (
|
||||
<div key={columnKey} className='objectKanbanHeaderCell'>
|
||||
{categoryPropertyDef ? (
|
||||
<ObjectProperty
|
||||
{...categoryPropertyDef}
|
||||
value={categoryValue}
|
||||
objectData={{ [categoryProperty]: categoryValue }}
|
||||
name={categoryProperty}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
@ -40,7 +56,8 @@ ObjectKanbanHeader.propTypes = {
|
||||
categoryValues: PropTypes.array.isRequired,
|
||||
categoryProperty: PropTypes.string.isRequired,
|
||||
categoryPropertyDef: PropTypes.object,
|
||||
trackRef: PropTypes.object
|
||||
trackRef: PropTypes.object,
|
||||
skeletonColumnCount: PropTypes.number
|
||||
}
|
||||
|
||||
export default ObjectKanbanHeader
|
||||
|
||||
@ -66,6 +66,7 @@ logger.setLevel(config.logLevel)
|
||||
|
||||
const SCROLL_THRESHOLD = 50
|
||||
const SKELETON_HEIGHT = 49.5
|
||||
const EMPTY_MASTER_FILTER = {}
|
||||
|
||||
const getCardColSpan = (containerWidth) => {
|
||||
if (containerWidth >= 2980) return 2
|
||||
@ -275,7 +276,7 @@ const ObjectTable = forwardRef(
|
||||
viewMode: viewModeProp,
|
||||
cards = false,
|
||||
visibleColumns = {},
|
||||
masterFilter = {},
|
||||
masterFilter,
|
||||
size = 'middle',
|
||||
onStateChange,
|
||||
showFilterSidebar = false,
|
||||
@ -303,10 +304,11 @@ const ObjectTable = forwardRef(
|
||||
const isCards = isCardsView(viewMode)
|
||||
const isKanban = isKanbanView(viewMode)
|
||||
const kanbanRef = useRef(null)
|
||||
const wasKanbanRef = useRef(false)
|
||||
const prevViewModeKeyRef = useRef(null)
|
||||
const { token, userProfile } = useContext(AuthContext)
|
||||
const { isElectron } = useContext(ElectronContext)
|
||||
const { callAction } = useActions()
|
||||
const resolvedMasterFilter = masterFilter ?? EMPTY_MASTER_FILTER
|
||||
const listViewId = objectListView?.listViewId ?? null
|
||||
const listViewFilter = objectListView?.listViewFilter ?? null
|
||||
const listViewSort = objectListView?.listViewSort ?? null
|
||||
@ -378,6 +380,7 @@ const ObjectTable = forwardRef(
|
||||
const tableSorterRef = useRef({})
|
||||
const listViewIdRef = useRef(listViewId)
|
||||
const listViewFilterRef = useRef(listViewFilter)
|
||||
const sidebarViewIdRef = useRef(listViewId)
|
||||
const activeObjectViewRef = useRef(activeObjectView)
|
||||
activeObjectViewRef.current = activeObjectView
|
||||
const onObjectViewFilterSortChangeRef = useRef(onObjectViewFilterSortChange)
|
||||
@ -393,13 +396,19 @@ const ObjectTable = forwardRef(
|
||||
listViewFilterRef.current = listViewFilter || {}
|
||||
}, [listViewId, listViewFilter])
|
||||
|
||||
const assignSidebarToView = useCallback((viewId) => {
|
||||
sidebarViewIdRef.current = viewId ?? null
|
||||
}, [])
|
||||
|
||||
const lastAppliedViewKeyRef = useRef(null)
|
||||
|
||||
const buildEffectiveFilter = useCallback(
|
||||
(userFilter, viewFilter = listViewFilterRef.current) => {
|
||||
const active = getActiveFilterValues(userFilter)
|
||||
const viewId = listViewIdRef.current
|
||||
if (!viewId) return active
|
||||
// While editing a view, the sidebar IS the view filter. Merging the
|
||||
// previous definition would put removed keys back.
|
||||
if (!viewId || activeObjectViewRef.current) return active
|
||||
return {
|
||||
...getActiveFilterValues(viewFilter || {}),
|
||||
...active
|
||||
@ -455,11 +464,11 @@ const ObjectTable = forwardRef(
|
||||
const subscribeToObjectTypeUpdatesFnRef = useRef(
|
||||
subscribeToObjectTypeUpdates
|
||||
)
|
||||
const prevValuesRef = useRef({ type, masterFilter })
|
||||
const prevValuesRef = useRef({ type, masterFilter: resolvedMasterFilter })
|
||||
|
||||
const getMasterFilter = useCallback(
|
||||
() => masterFilter || {},
|
||||
[masterFilter]
|
||||
() => resolvedMasterFilter || EMPTY_MASTER_FILTER,
|
||||
[resolvedMasterFilter]
|
||||
)
|
||||
|
||||
const resolveSorter = useCallback((userSorter) => {
|
||||
@ -1042,15 +1051,26 @@ const ObjectTable = forwardRef(
|
||||
// view definition + any user-entered sidebar filters.
|
||||
const effectiveFilter = useMemo(() => {
|
||||
const userFilter = getActiveFilterValues(sidebarFilter)
|
||||
if (!listViewId) return userFilter
|
||||
const sidebarMatchesView = sidebarViewIdRef.current === listViewId
|
||||
|
||||
if (!listViewId || activeObjectView) {
|
||||
if (!listViewId && !sidebarMatchesView) return {}
|
||||
return userFilter
|
||||
}
|
||||
|
||||
const viewFilter = getActiveFilterValues(listViewFilter || {})
|
||||
if (!sidebarMatchesView) {
|
||||
return viewFilter
|
||||
}
|
||||
return {
|
||||
...getActiveFilterValues(listViewFilter || {}),
|
||||
...viewFilter,
|
||||
...userFilter
|
||||
}
|
||||
}, [listViewId, listViewFilter, sidebarFilter])
|
||||
}, [activeObjectView, listViewId, listViewFilter, sidebarFilter])
|
||||
|
||||
const effectiveSorter = useMemo(() => {
|
||||
if (tableSorter?.field && tableSorter?.order) {
|
||||
const sorterMatchesView = sidebarViewIdRef.current === listViewId
|
||||
if (sorterMatchesView && tableSorter?.field && tableSorter?.order) {
|
||||
return { field: tableSorter.field, order: tableSorter.order }
|
||||
}
|
||||
if (listViewId && listViewSort?.field && listViewSort?.order) {
|
||||
@ -1060,16 +1080,16 @@ const ObjectTable = forwardRef(
|
||||
}, [listViewId, listViewSort, tableSorter])
|
||||
|
||||
const subscriptionFilter = useMemo(
|
||||
() => ({ ...masterFilter, ...effectiveFilter }),
|
||||
[effectiveFilter, masterFilter]
|
||||
() => ({ ...resolvedMasterFilter, ...effectiveFilter }),
|
||||
[effectiveFilter, resolvedMasterFilter]
|
||||
)
|
||||
|
||||
const tableFilterContextValue = useMemo(
|
||||
() => ({
|
||||
filter: effectiveFilter,
|
||||
masterFilter
|
||||
masterFilter: resolvedMasterFilter
|
||||
}),
|
||||
[effectiveFilter, masterFilter]
|
||||
[effectiveFilter, resolvedMasterFilter]
|
||||
)
|
||||
|
||||
newEventHandlerRef.current = newEventHandler
|
||||
@ -1142,27 +1162,33 @@ const ObjectTable = forwardRef(
|
||||
const loadPage = useCallback(
|
||||
async (pageNum, filter = null, sorter = null) => {
|
||||
if (isKanban) {
|
||||
const generation = dataLoadGenerationRef.current + 1
|
||||
dataLoadGenerationRef.current = generation
|
||||
const activeFilter =
|
||||
filter != null ? filter : activeFilterRef.current
|
||||
const resolvedSorter =
|
||||
sorter != null
|
||||
? resolveSorter(sorter)
|
||||
: resolveSorter(tableSorterRef.current)
|
||||
const sorterForLoad = resolvedSorter?.field ? resolvedSorter : {}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const activeFilter =
|
||||
filter != null ? filter : activeFilterRef.current
|
||||
const resolvedSorter =
|
||||
sorter != null
|
||||
? resolveSorter(sorter)
|
||||
: resolveSorter(tableSorterRef.current)
|
||||
|
||||
if (!kanbanRef.current?.load) {
|
||||
let loadKanban = kanbanRef.current?.load
|
||||
if (!loadKanban) {
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve))
|
||||
if (generation !== dataLoadGenerationRef.current) return
|
||||
loadKanban = kanbanRef.current?.load
|
||||
}
|
||||
|
||||
await kanbanRef.current?.load?.(
|
||||
activeFilter,
|
||||
resolvedSorter?.field ? resolvedSorter : {}
|
||||
)
|
||||
if (!loadKanban) return
|
||||
await loadKanban(activeFilter, sorterForLoad)
|
||||
} catch (error) {
|
||||
logger.error('Error loading kanban view:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
if (generation === dataLoadGenerationRef.current) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -1229,6 +1255,7 @@ const ObjectTable = forwardRef(
|
||||
const resolvedSorter = resolveSorter(sorter)
|
||||
|
||||
if (Object.keys(filter).length > 0) {
|
||||
assignSidebarToView(listViewIdRef.current)
|
||||
setSidebarFilter(filter)
|
||||
}
|
||||
if (sorter?.field) {
|
||||
@ -1242,6 +1269,7 @@ const ObjectTable = forwardRef(
|
||||
resolvedSorter.field ? resolvedSorter : null
|
||||
)
|
||||
}, [
|
||||
assignSidebarToView,
|
||||
getPersistedFilter,
|
||||
getPersistedSorter,
|
||||
getViewFromUrl,
|
||||
@ -1249,6 +1277,8 @@ const ObjectTable = forwardRef(
|
||||
loadPage,
|
||||
resolveSorter
|
||||
])
|
||||
const loadInitialPageRef = useRef(loadInitialPage)
|
||||
loadInitialPageRef.current = loadInitialPage
|
||||
|
||||
// When a view becomes available, apply its definition once. Skip empty
|
||||
// prop-lag updates so we never clear an already-applied view filter.
|
||||
@ -1275,6 +1305,7 @@ const ObjectTable = forwardRef(
|
||||
|
||||
lastAppliedViewKeyRef.current = viewKey
|
||||
|
||||
assignSidebarToView(listViewId)
|
||||
setSidebarFilter(viewFilter)
|
||||
setTableSorter(viewSort)
|
||||
tableSorterRef.current = viewSort
|
||||
@ -1307,6 +1338,7 @@ const ObjectTable = forwardRef(
|
||||
listViewIdRef.current = null
|
||||
listViewFilterRef.current = {}
|
||||
lastAppliedViewKeyRef.current = null
|
||||
assignSidebarToView(null)
|
||||
filter = getAllTabPersistedFilter()
|
||||
sorter = getAllTabPersistedSorter()
|
||||
|
||||
@ -1352,6 +1384,7 @@ const ObjectTable = forwardRef(
|
||||
|
||||
lastAppliedViewKeyRef.current = `${viewId || ''}:${JSON.stringify(activeFilter)}:${JSON.stringify(nextSorter)}`
|
||||
|
||||
assignSidebarToView(viewId)
|
||||
setSidebarFilter(filter)
|
||||
tableSorterRef.current = nextSorter
|
||||
setTableSorter(nextSorter)
|
||||
@ -1364,6 +1397,7 @@ const ObjectTable = forwardRef(
|
||||
)
|
||||
},
|
||||
[
|
||||
assignSidebarToView,
|
||||
getAllTabPersistedFilter,
|
||||
getAllTabPersistedSorter,
|
||||
getViewFromUrl,
|
||||
@ -1396,7 +1430,7 @@ const ObjectTable = forwardRef(
|
||||
let cancelled = false
|
||||
|
||||
const run = async () => {
|
||||
await loadInitialPage()
|
||||
await loadInitialPageRef.current()
|
||||
if (!cancelled) {
|
||||
setInitialized(true)
|
||||
}
|
||||
@ -1407,35 +1441,34 @@ const ObjectTable = forwardRef(
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [token, loadInitialPage, initialized, connected])
|
||||
}, [token, initialized, connected])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isKanban || connected !== true || token == null) {
|
||||
wasKanbanRef.current = isKanban
|
||||
const viewModeKey = `${viewMode.type}:${viewMode?.settings?.categoryProperty || ''}`
|
||||
|
||||
if (connected !== true || token == null || !initialized) {
|
||||
prevViewModeKeyRef.current = viewModeKey
|
||||
return
|
||||
}
|
||||
|
||||
const categoryProperty = viewMode?.settings?.categoryProperty
|
||||
if (!categoryProperty) {
|
||||
wasKanbanRef.current = isKanban
|
||||
return
|
||||
}
|
||||
if (prevViewModeKeyRef.current === viewModeKey) return
|
||||
prevViewModeKeyRef.current = viewModeKey
|
||||
|
||||
if (!wasKanbanRef.current) {
|
||||
loadPage(
|
||||
initialPage,
|
||||
activeFilterRef.current,
|
||||
tableSorterRef.current?.field ? tableSorterRef.current : null
|
||||
)
|
||||
}
|
||||
if (isKanban && !viewMode?.settings?.categoryProperty) return
|
||||
|
||||
wasKanbanRef.current = isKanban
|
||||
loadPage(
|
||||
initialPage,
|
||||
activeFilterRef.current,
|
||||
tableSorterRef.current?.field ? tableSorterRef.current : null
|
||||
)
|
||||
}, [
|
||||
isKanban,
|
||||
connected,
|
||||
token,
|
||||
initialized,
|
||||
loadPage,
|
||||
initialPage,
|
||||
viewMode.type,
|
||||
viewMode?.settings?.categoryProperty
|
||||
])
|
||||
|
||||
@ -1445,7 +1478,8 @@ const ObjectTable = forwardRef(
|
||||
|
||||
const hasChanged =
|
||||
prevValues.type !== type ||
|
||||
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
|
||||
JSON.stringify(prevValues.masterFilter) !==
|
||||
JSON.stringify(resolvedMasterFilter)
|
||||
|
||||
if (hasChanged) {
|
||||
beginDataReload()
|
||||
@ -1453,14 +1487,15 @@ const ObjectTable = forwardRef(
|
||||
setPages([])
|
||||
activeFilterRef.current = {}
|
||||
tableSorterRef.current = {}
|
||||
assignSidebarToView(null)
|
||||
setSidebarFilter({})
|
||||
setTableSorter({})
|
||||
setInitialized(false)
|
||||
setLoading(true)
|
||||
setLazyLoading(false)
|
||||
prevValuesRef.current = { type, masterFilter }
|
||||
prevValuesRef.current = { type, masterFilter: resolvedMasterFilter }
|
||||
}
|
||||
}, [type, masterFilter, beginDataReload])
|
||||
}, [type, resolvedMasterFilter, beginDataReload])
|
||||
|
||||
useEffect(() => {
|
||||
registerPageFilter(effectiveFilter)
|
||||
@ -1493,7 +1528,7 @@ const ObjectTable = forwardRef(
|
||||
propertyLabel={propertyLabel}
|
||||
modelType={type}
|
||||
filter={effectiveFilter}
|
||||
masterFilter={masterFilter}
|
||||
masterFilter={resolvedMasterFilter}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1517,25 +1552,27 @@ const ObjectTable = forwardRef(
|
||||
}
|
||||
|
||||
setSidebarFilter(next)
|
||||
assignSidebarToView(listViewIdRef.current)
|
||||
setTableSorter(nextSorter)
|
||||
const effective = buildEffectiveFilter(next)
|
||||
activeFilterRef.current = effective
|
||||
if (activeObjectViewRef.current) {
|
||||
listViewFilterRef.current = next
|
||||
// Editing a view: keep filter/sort on the draft view only
|
||||
onObjectViewFilterSortChangeRef.current?.(next, nextSorter)
|
||||
} else if (!listViewIdRef.current) {
|
||||
// All tab: persist personal filter/sort to URL/session
|
||||
persistTableState(next, nextSorter)
|
||||
}
|
||||
const effective = buildEffectiveFilter(next)
|
||||
activeFilterRef.current = effective
|
||||
loadPage(initialPage, effective, resolveSorter(nextSorter))
|
||||
}
|
||||
|
||||
const handleSidebarFilterChange = useCallback(
|
||||
(newSidebarFilter) => {
|
||||
setSidebarFilter(newSidebarFilter)
|
||||
const effective = buildEffectiveFilter(newSidebarFilter)
|
||||
activeFilterRef.current = effective
|
||||
assignSidebarToView(listViewIdRef.current)
|
||||
if (activeObjectViewRef.current) {
|
||||
listViewFilterRef.current = newSidebarFilter
|
||||
onObjectViewFilterSortChangeRef.current?.(
|
||||
newSidebarFilter,
|
||||
tableSorter
|
||||
@ -1543,9 +1580,12 @@ const ObjectTable = forwardRef(
|
||||
} else if (!listViewIdRef.current) {
|
||||
persistFilter(newSidebarFilter)
|
||||
}
|
||||
const effective = buildEffectiveFilter(newSidebarFilter)
|
||||
activeFilterRef.current = effective
|
||||
loadPage(initialPage, effective, resolveSorter(tableSorter))
|
||||
},
|
||||
[
|
||||
assignSidebarToView,
|
||||
buildEffectiveFilter,
|
||||
initialPage,
|
||||
loadPage,
|
||||
@ -1568,11 +1608,8 @@ const ObjectTable = forwardRef(
|
||||
} else if (!listViewIdRef.current) {
|
||||
persistSort(nextSorter)
|
||||
}
|
||||
loadPage(
|
||||
initialPage,
|
||||
buildEffectiveFilter(sidebarFilter),
|
||||
resolveSorter(nextSorter)
|
||||
)
|
||||
const effective = buildEffectiveFilter(sidebarFilter)
|
||||
loadPage(initialPage, effective, resolveSorter(nextSorter))
|
||||
},
|
||||
[
|
||||
buildEffectiveFilter,
|
||||
@ -1711,7 +1748,10 @@ const ObjectTable = forwardRef(
|
||||
}
|
||||
}
|
||||
|
||||
if (isFilterable && !Object.keys(masterFilter).includes(prop.name)) {
|
||||
if (
|
||||
isFilterable &&
|
||||
!Object.keys(resolvedMasterFilter).includes(prop.name)
|
||||
) {
|
||||
columnConfig.filterDropdown = ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
@ -1851,7 +1891,9 @@ const ObjectTable = forwardRef(
|
||||
return (
|
||||
<ScrollBox
|
||||
style={{
|
||||
maxHeight: `100%`
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
minHeight: 0
|
||||
}}
|
||||
>
|
||||
<div className='objectTableCards'>
|
||||
@ -1944,7 +1986,7 @@ const ObjectTable = forwardRef(
|
||||
type={type}
|
||||
categoryProperty={viewMode.settings?.categoryProperty}
|
||||
baseFilter={effectiveFilter}
|
||||
masterFilter={masterFilter}
|
||||
masterFilter={resolvedMasterFilter}
|
||||
sorter={effectiveSorter}
|
||||
pageSize={pageSize}
|
||||
model={model}
|
||||
@ -1964,7 +2006,7 @@ const ObjectTable = forwardRef(
|
||||
<Spin
|
||||
indicator={<LoadingOutlined />}
|
||||
spinning={loading}
|
||||
style={{ height: '100%' }}
|
||||
style={{ height: '100%', flex: 1, minHeight: 0 }}
|
||||
>
|
||||
{renderCards()}
|
||||
</Spin>
|
||||
@ -2012,9 +2054,9 @@ const ObjectTable = forwardRef(
|
||||
<Flex style={{ flex: 1, minHeight: 0 }}>
|
||||
<FilterSidebar
|
||||
type={type}
|
||||
filter={effectiveFilter}
|
||||
filter={activeObjectView ? sidebarFilter : effectiveFilter}
|
||||
onFilterChange={handleSidebarFilterChange}
|
||||
masterFilter={masterFilter}
|
||||
masterFilter={resolvedMasterFilter}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
@ -119,7 +119,7 @@ const ObjectTableViewButton = ({
|
||||
).map((option) => (
|
||||
<Radio key={option.type} value={option.type}>
|
||||
<Flex
|
||||
gap='4px'
|
||||
gap='8px'
|
||||
align='center'
|
||||
style={{ marginLeft: '4px' }}
|
||||
>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user