Tom Butcher 4cae70b5e8 Integrate AuthContext into ObjectKanban and ObjectTimeline for Enhanced Subscription Management
- Added AuthContext to both ObjectKanban and ObjectTimeline components to manage subscriptions based on user authentication status.
- Updated useEffect hooks to conditionally subscribe to updates only when the user is connected and authenticated, improving performance and reliability.
- Refactored dependency arrays in useEffect hooks to include token checks, ensuring proper cleanup and subscription management.
2026-09-05 01:58:55 +01:00

1100 lines
34 KiB
JavaScript

import {
forwardRef,
useCallback,
useContext,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState
} from 'react'
import { Flex } from 'antd'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import ObjectCard from './ObjectCard'
import ObjectKanbanColumn from './ObjectKanbanColumn'
import ObjectKanbanHeader from './ObjectKanbanHeader'
import ScrollBox from './ScrollBox'
import Spin from './Spin'
import {
getCategoryColumnKeys,
getCategoryValueKey,
KANBAN_DEFAULT_COLUMN_WIDTH,
KANBAN_MIN_COLUMN_WIDTH,
resolveKanbanColumns,
toKanbanColumnsConfig
} from './viewModeUtils'
import { areValuesEqual } from '../utils/Utils'
import MissingPlaceholder from './MissingPlaceholder'
const KANBAN_COLUMN_WIDTH = KANBAN_DEFAULT_COLUMN_WIDTH
const KANBAN_GAP = 16
const KANBAN_CARD_MIN_HEIGHT = 136
const reorderColumnsByKey = (columns, fromKey, toKey) => {
if (!fromKey || !toKey || fromKey === toKey) return columns
const fromIndex = columns.findIndex((column) => column.key === fromKey)
const toIndex = columns.findIndex((column) => column.key === toKey)
if (fromIndex < 0 || toIndex < 0) return columns
const next = [...columns]
const [moved] = next.splice(fromIndex, 1)
next.splice(toIndex, 0, moved)
return next
}
const getUpdateKeys = (updated) => {
if (!updated || typeof updated !== 'object') return []
return Object.keys(updated).filter(
(key) => key !== '_id' && key !== 'objectType'
)
}
const getAffectingFilterOrSortKeys = (
existingItem,
updatedData,
filter,
masterFilter,
sorter,
categoryProperty
) => {
const updateKeys = getUpdateKeys(updatedData)
if (!updateKeys.length) return []
const filterKeys = new Set([
...Object.keys(filter || {}),
...Object.keys(masterFilter || {})
])
if (categoryProperty) filterKeys.add(categoryProperty)
const isAffectingKey = (key) =>
filterKeys.has(key) || (sorter?.field && key === sorter.field)
// Item not in the current fetch: any filter/sort key in the update
// may change whether it belongs in this view.
if (!existingItem) {
return updateKeys.filter(isAffectingKey)
}
// Item already fetched: reload when a filter or sort key's value changed.
return updateKeys.filter(
(key) =>
isAffectingKey(key) &&
!areValuesEqual(existingItem[key], updatedData[key])
)
}
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 ObjectKanbanColumnResizeHandle = ({ active = false, onMouseDown }) => (
<div
className={classNames('objectKanbanColumnResizeHandle', { active })}
role='separator'
aria-orientation='vertical'
aria-label='Resize column'
onMouseDown={onMouseDown}
/>
)
ObjectKanbanColumnResizeHandle.propTypes = {
active: PropTypes.bool,
onMouseDown: PropTypes.func
}
const ObjectKanbanColumnResizeOverlay = ({
columnKey,
width,
columnIndex,
isResizing,
onResizeStart
}) => {
return (
<div
className='objectKanbanColumnResizeOverlay'
key={columnIndex}
style={{
width,
flex: `0 0 ${width}px`
}}
>
{onResizeStart && columnKey && (
<ObjectKanbanColumnResizeHandle
active={isResizing}
onMouseDown={(event) => onResizeStart(event, columnKey)}
/>
)}
</div>
)
}
const ObjectKanbanColumnSkeleton = ({
columnKey,
width,
cardCount = 0,
model,
modelProperties,
visibleColumns,
keyPrefix = 'skeleton',
columnIndex = 0
}) => (
<div
className='objectKanbanColumnSkeleton'
style={{
width,
flex: `0 0 ${width}px`
}}
>
<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}-${columnKey ?? columnIndex}-${cardIndex}`
}}
cardStyle='bordered'
/>
))}
</Flex>
)}
</div>
</div>
)
ObjectKanbanColumnSkeleton.propTypes = {
columnKey: PropTypes.string,
width: PropTypes.number.isRequired,
cardCount: PropTypes.number,
model: PropTypes.object.isRequired,
modelProperties: PropTypes.array.isRequired,
visibleColumns: PropTypes.object,
keyPrefix: PropTypes.string,
columnIndex: PropTypes.number,
isResizing: PropTypes.bool,
onResizeStart: PropTypes.func
}
const ObjectKanbanSkeletonColumns = ({
columnCount,
cardCount = 0,
trackRef,
model,
modelProperties,
visibleColumns,
keyPrefix = 'skeleton',
columns = null,
resizingKey = null,
onColumnResizeStart
}) => {
const skeletonColumns =
columns?.length > 0
? columns
: Array.from({ length: columnCount }).map((_, columnIndex) => ({
key: `${keyPrefix}-${columnIndex}`,
width: KANBAN_COLUMN_WIDTH
}))
return (
<div className='objectKanbanSkeletonColumns objectKanbanSkeletonColumns--loading'>
<div className='objectKanbanSkeletonTrack' ref={trackRef}>
<Flex gap='middle' className='objectKanbanSkeletonRow'>
{skeletonColumns.map((column, columnIndex) => (
<ObjectKanbanColumnSkeleton
key={column.key || `${keyPrefix}-${columnIndex}`}
columnKey={column.key}
width={column.width}
cardCount={cardCount}
model={model}
modelProperties={modelProperties}
visibleColumns={visibleColumns}
keyPrefix={keyPrefix}
columnIndex={columnIndex}
isResizing={resizingKey === column.key}
onResizeStart={onColumnResizeStart}
/>
))}
</Flex>
</div>
</div>
)
}
ObjectKanbanColumnResizeOverlay.propTypes = {
columnKey: PropTypes.string,
width: PropTypes.number.isRequired,
columnIndex: PropTypes.number,
isResizing: PropTypes.bool,
onResizeStart: PropTypes.func
}
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,
columns: PropTypes.array,
resizingKey: PropTypes.string,
onColumnResizeStart: PropTypes.func
}
const ObjectKanban = forwardRef(
(
{
type,
categoryProperty,
baseFilter = {},
masterFilter = {},
sorter = {},
lazyLoading = false,
pageSize = 25,
model,
modelProperties,
visibleColumns = {},
isEditing = false,
rowActions = [],
renderActions,
columns: storedColumns = [],
onColumnsChange
},
ref
) => {
const {
getModelPropertyValues,
connected,
subscribeToAllObjectUpdates,
subscribeToObjectTypeUpdates
} = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
const columnRefs = useRef({})
const headerTrackRef = useRef(null)
const skeletonTrackRef = useRef(null)
const overlayTrackRef = 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 [reloadingColumnKeys, setReloadingColumnKeys] = useState([])
const [draggedKey, setDraggedKey] = useState(null)
const [dropTargetKey, setDropTargetKey] = useState(null)
const [widthOverrides, setWidthOverrides] = useState({})
const [optimisticColumns, setOptimisticColumns] = useState(null)
const [resizingKey, setResizingKey] = useState(null)
const baseFilterRef = useRef(baseFilter)
const masterFilterRef = useRef(masterFilter)
const sorterRef = useRef(sorter)
const categoryPropertyRef = useRef(categoryProperty)
const getModelPropertyValuesRef = useRef(getModelPropertyValues)
const lastCategoryQueryKeyRef = useRef(null)
const hasLoadedRef = useRef(false)
const loadGenerationRef = useRef(0)
const updateEventHandlerRef = useRef()
const newEventHandlerRef = useRef()
const silentReloadRef = useRef(null)
const silentReloadItemColumnsRef = useRef(null)
const silentReloadCategoryColumnsRef = useRef(null)
const categoryValuesRef = useRef([])
const subscriptionFilterRef = useRef({})
const subscribeToObjectTypeUpdatesRef = useRef(null)
const subscribeToAllObjectUpdatesRef = useRef(null)
const subscribedTypeRef = useRef(null)
const subscribeToObjectTypeUpdatesFnRef = useRef(
subscribeToObjectTypeUpdates
)
baseFilterRef.current = baseFilter
masterFilterRef.current = masterFilter
sorterRef.current = sorter
categoryPropertyRef.current = categoryProperty
getModelPropertyValuesRef.current = getModelPropertyValues
subscribeToObjectTypeUpdatesFnRef.current = subscribeToObjectTypeUpdates
categoryValuesRef.current = categoryValues
const setScrollContainerRef = useCallback((node) => {
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
const skeletonTrack = skeletonTrackRef.current
const overlayTrack = overlayTrackRef.current
if (!mainScroll || !headerTrack) return undefined
const handleMainScroll = () => {
const offset = `translate3d(-${mainScroll.scrollLeft}px, 0, 0)`
headerTrack.style.transform = offset
if (skeletonTrack) {
skeletonTrack.style.transform = offset
}
if (overlayTrack) {
overlayTrack.style.transform = offset
}
}
handleMainScroll()
mainScroll.addEventListener('scroll', handleMainScroll, { passive: true })
return () => {
mainScroll.removeEventListener('scroll', handleMainScroll)
}
}, [scrollElement, categoryValues])
const categoryPropertyDef = useMemo(
() =>
model?.properties?.find(
(property) => property.name === categoryProperty
),
[categoryProperty, model]
)
const buildCategoryQueryKey = useCallback(
(filter, activeSorter) =>
JSON.stringify({
type,
categoryProperty,
filter,
masterFilter: masterFilterRef.current,
sorter: activeSorter
}),
[categoryProperty, type]
)
const reloadColumns = useCallback(async (columnKeys = null) => {
const refs =
columnKeys == null
? Object.values(columnRefs.current)
: columnKeys.map((key) => columnRefs.current[key])
await Promise.all(
refs.filter(Boolean).map((columnRef) => columnRef.reload?.())
)
}, [])
const findItem = useCallback((id) => {
for (const columnRef of Object.values(columnRefs.current)) {
const item = columnRef?.findItem?.(id)
if (item) return item
}
return null
}, [])
const findItemColumnKeys = useCallback((id) => {
return Object.entries(columnRefs.current)
.filter(([, columnRef]) => columnRef?.findItem?.(id))
.map(([key]) => key)
}, [])
const updateItem = useCallback((id, updatedData) => {
for (const columnRef of Object.values(columnRefs.current)) {
if (columnRef?.findItem?.(id)) {
columnRef.updateItem?.(id, updatedData)
}
}
}, [])
const load = useCallback(
async (filter = null, sorterArg = null, options = {}) => {
const silent = options.silent === true
const generation = ++loadGenerationRef.current
if (!categoryProperty) {
lastCategoryQueryKeyRef.current = null
hasLoadedRef.current = true
categoryValuesRef.current = []
setCategoryValues([])
setHasLoaded(true)
return []
}
const activeFilter = filter ?? baseFilterRef.current
const activeSorter = sorterArg ?? sorterRef.current
const activeMasterFilter = masterFilterRef.current
const queryKey = buildCategoryQueryKey(activeFilter, activeSorter)
const canReuseColumns =
lastCategoryQueryKeyRef.current === queryKey && hasLoadedRef.current
if (canReuseColumns) {
await reloadColumns()
if (generation !== loadGenerationRef.current) {
return categoryValuesRef.current
}
hasLoadedRef.current = true
setHasLoaded(true)
return categoryValuesRef.current
}
// Filter/sort changes keep current columns visible; objectView /
// initial loads clear to skeleton + Spin.
if (!silent || !hasLoadedRef.current) {
hasLoadedRef.current = false
setHasLoaded(false)
}
try {
const values = await getModelPropertyValuesRef.current(
type,
categoryProperty,
{
filter: activeFilter,
masterFilter: activeMasterFilter
}
)
if (generation !== loadGenerationRef.current) {
return categoryValuesRef.current
}
const nextValues = Array.isArray(values) ? values : []
lastCategoryQueryKeyRef.current = queryKey
hasLoadedRef.current = true
setLoadedFilter(activeFilter)
setLoadedSorter(activeSorter)
categoryValuesRef.current = nextValues
setCategoryValues(nextValues)
setHasLoaded(true)
return nextValues
} catch (error) {
if (generation !== loadGenerationRef.current) {
return categoryValuesRef.current
}
console.error('Error fetching kanban category values:', error)
lastCategoryQueryKeyRef.current = null
hasLoadedRef.current = true
categoryValuesRef.current = []
setCategoryValues([])
setHasLoaded(true)
throw error
}
},
[buildCategoryQueryKey, categoryProperty, reloadColumns, type]
)
const reload = useCallback(async () => {
lastCategoryQueryKeyRef.current = null
hasLoadedRef.current = false
await load(baseFilterRef.current, sorterRef.current)
}, [load])
const silentReload = useCallback(async () => {
const previousKeys = categoryValuesRef.current.map(getCategoryValueKey)
setReloadingColumnKeys(previousKeys)
try {
lastCategoryQueryKeyRef.current = null
const nextValues = await load(
baseFilterRef.current,
sorterRef.current,
{
silent: true
}
)
const nextKeySet = new Set((nextValues || []).map(getCategoryValueKey))
// Only reload columns that existed before; newly created columns
// mount and load themselves.
const keysToReload = previousKeys.filter((key) => nextKeySet.has(key))
setReloadingColumnKeys(keysToReload)
await reloadColumns(keysToReload)
} finally {
setReloadingColumnKeys([])
}
}, [load, reloadColumns])
const silentReloadItemColumns = useCallback(
async (id) => {
const keysToReload = findItemColumnKeys(id)
if (!keysToReload.length) {
await silentReload()
return
}
setReloadingColumnKeys(keysToReload)
try {
await reloadColumns(keysToReload)
} finally {
setReloadingColumnKeys([])
}
},
[findItemColumnKeys, reloadColumns, silentReload]
)
const silentReloadCategoryColumns = useCallback(
async (existingItem, updatedData) => {
const categoryPropertyName = categoryPropertyRef.current
const previousKeys = new Set(
categoryValuesRef.current.map(getCategoryValueKey)
)
const oldKeys =
existingItem && categoryPropertyName
? getCategoryColumnKeys(existingItem[categoryPropertyName])
: []
const updatedKeys =
categoryPropertyName &&
updatedData &&
Object.prototype.hasOwnProperty.call(
updatedData,
categoryPropertyName
)
? getCategoryColumnKeys(updatedData[categoryPropertyName]).filter(
(key) => previousKeys.has(key)
)
: []
const tentativeKeys = [...new Set([...oldKeys, ...updatedKeys])]
setReloadingColumnKeys(tentativeKeys)
try {
lastCategoryQueryKeyRef.current = null
const nextValues = await load(
baseFilterRef.current,
sorterRef.current,
{ silent: true }
)
const nextKeySet = new Set(
(nextValues || []).map(getCategoryValueKey)
)
const keysToReload = []
for (const oldColumnKey of oldKeys) {
if (nextKeySet.has(oldColumnKey)) {
keysToReload.push(oldColumnKey)
}
}
if (
categoryPropertyName &&
updatedData &&
Object.prototype.hasOwnProperty.call(
updatedData,
categoryPropertyName
)
) {
for (const newColumnKey of getCategoryColumnKeys(
updatedData[categoryPropertyName]
)) {
// Newly created columns mount and fetch themselves — skip reload.
if (
previousKeys.has(newColumnKey) &&
nextKeySet.has(newColumnKey)
) {
keysToReload.push(newColumnKey)
}
}
}
const uniqueKeys = [...new Set(keysToReload)]
setReloadingColumnKeys(uniqueKeys)
await reloadColumns(uniqueKeys)
} finally {
setReloadingColumnKeys([])
}
},
[load, reloadColumns]
)
silentReloadRef.current = silentReload
silentReloadItemColumnsRef.current = silentReloadItemColumns
silentReloadCategoryColumnsRef.current = silentReloadCategoryColumns
const subscriptionFilter = useMemo(
() => ({ ...masterFilter, ...baseFilter }),
[baseFilter, masterFilter]
)
subscriptionFilterRef.current = subscriptionFilter
const updateEventHandler = useCallback(
(id, updatedData) => {
const existingItem = findItem(id)
// Always merge into the local item when it already exists.
if (existingItem) {
updateItem(id, updatedData)
}
const affectingKeys = getAffectingFilterOrSortKeys(
existingItem,
updatedData,
subscriptionFilterRef.current,
{},
sorterRef.current,
categoryPropertyRef.current
)
if (!affectingKeys.length) return
const categoryPropertyName = categoryPropertyRef.current
if (
categoryPropertyName &&
affectingKeys.includes(categoryPropertyName)
) {
silentReloadCategoryColumnsRef.current?.(existingItem, updatedData)
return
}
const sorterField = sorterRef.current?.field
const onlySortKeyChanged =
existingItem &&
sorterField &&
affectingKeys.length === 1 &&
affectingKeys[0] === sorterField
if (onlySortKeyChanged) {
silentReloadItemColumnsRef.current?.(id)
return
}
silentReloadRef.current?.()
},
[findItem, updateItem]
)
updateEventHandlerRef.current = updateEventHandler
const newEventHandler = useCallback(() => {
silentReloadRef.current?.()
}, [])
newEventHandlerRef.current = newEventHandler
useEffect(() => {
return () => {
if (
connected == true &&
token &&
subscribeToObjectTypeUpdatesRef.current
) {
subscribeToObjectTypeUpdatesRef.current()
subscribeToObjectTypeUpdatesRef.current = null
}
if (
connected == true &&
token &&
subscribeToAllObjectUpdatesRef.current
) {
subscribeToAllObjectUpdatesRef.current()
subscribeToAllObjectUpdatesRef.current = null
}
}
}, [connected, token])
useEffect(() => {
if (connected !== true || !type || !token) return
const unsubscribe = subscribeToAllObjectUpdates(
type,
(id, updateData) => {
updateEventHandlerRef.current(id, updateData)
}
)
subscribeToAllObjectUpdatesRef.current = unsubscribe
return () => {
if (unsubscribe) unsubscribe()
if (subscribeToAllObjectUpdatesRef.current === unsubscribe) {
subscribeToAllObjectUpdatesRef.current = null
}
}
}, [type, connected, subscribeToAllObjectUpdates, token])
useEffect(() => {
if (connected !== true || !token) return
if (subscribedTypeRef.current === type) return
const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current(
type,
subscriptionFilterRef.current,
(params) => newEventHandlerRef.current(params)
)
subscribeToObjectTypeUpdatesRef.current = unsubscribe
subscribedTypeRef.current = type
return () => {
if (unsubscribe) unsubscribe()
if (subscribeToObjectTypeUpdatesRef.current === unsubscribe) {
subscribeToObjectTypeUpdatesRef.current = null
}
if (subscribedTypeRef.current === type) {
subscribedTypeRef.current = null
}
}
}, [type, connected, token])
useImperativeHandle(
ref,
() => ({
load,
reload
}),
[load, reload]
)
const resolvedColumns = useMemo(() => {
const fromStore = resolveKanbanColumns(categoryValues, storedColumns)
if (!optimisticColumns) return fromStore
const valueByKey = new Map(
fromStore.map((column) => [column.key, column.value])
)
const ordered = []
const seen = new Set()
for (const column of optimisticColumns) {
if (!valueByKey.has(column.key) || seen.has(column.key)) continue
ordered.push({
key: column.key,
value: valueByKey.get(column.key),
width: column.width
})
seen.add(column.key)
}
for (const column of fromStore) {
if (seen.has(column.key)) continue
ordered.push(column)
}
return ordered
}, [categoryValues, optimisticColumns, storedColumns])
useEffect(() => {
setOptimisticColumns(null)
setWidthOverrides({})
setDraggedKey(null)
setDropTargetKey(null)
setResizingKey(null)
}, [categoryProperty])
useEffect(() => {
if (!optimisticColumns) return
const fromStore = resolveKanbanColumns(categoryValues, storedColumns)
if (
JSON.stringify(toKanbanColumnsConfig(fromStore)) ===
JSON.stringify(toKanbanColumnsConfig(optimisticColumns))
) {
setOptimisticColumns(null)
}
}, [categoryValues, optimisticColumns, storedColumns])
const displayColumns = useMemo(() => {
if (!Object.keys(widthOverrides).length) return resolvedColumns
return resolvedColumns.map((column) =>
widthOverrides[column.key] != null
? { ...column, width: widthOverrides[column.key] }
: column
)
}, [resolvedColumns, widthOverrides])
const persistColumns = useCallback(
(nextColumns) => {
setOptimisticColumns(nextColumns)
onColumnsChange?.(toKanbanColumnsConfig(nextColumns))
},
[onColumnsChange]
)
const handleColumnDragStart = useCallback((event, columnKey) => {
event.dataTransfer.effectAllowed = 'move'
try {
event.dataTransfer.setData('text/plain', columnKey)
} catch {
// Some browsers restrict setData outside certain types.
}
setDraggedKey(columnKey)
setDropTargetKey(null)
}, [])
const handleColumnDragOver = useCallback(
(event, columnKey) => {
if (!draggedKey || draggedKey === columnKey) return
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
setDropTargetKey((prev) => (prev === columnKey ? prev : columnKey))
},
[draggedKey]
)
const finishColumnReorder = useCallback(
(fromKey, toKey) => {
if (!fromKey || !toKey || fromKey === toKey) {
setDraggedKey(null)
setDropTargetKey(null)
return
}
const nextColumns = reorderColumnsByKey(resolvedColumns, fromKey, toKey)
persistColumns(nextColumns)
setDraggedKey(null)
setDropTargetKey(null)
},
[persistColumns, resolvedColumns]
)
const handleColumnDrop = useCallback(
(event, columnKey) => {
event.preventDefault()
const fromKey = draggedKey || event.dataTransfer.getData('text/plain')
finishColumnReorder(fromKey, columnKey)
},
[draggedKey, finishColumnReorder]
)
const handleColumnDragEnd = useCallback(() => {
setDraggedKey(null)
setDropTargetKey(null)
}, [])
const handleColumnResizeStart = useCallback(
(event, columnKey) => {
event.preventDefault()
event.stopPropagation()
const startX = event.clientX
const startWidth =
displayColumns.find((column) => column.key === columnKey)?.width ??
KANBAN_COLUMN_WIDTH
let currentWidth = startWidth
setResizingKey(columnKey)
const handleMouseMove = (moveEvent) => {
currentWidth = Math.max(
KANBAN_MIN_COLUMN_WIDTH,
startWidth + (moveEvent.clientX - startX)
)
setWidthOverrides((prev) => ({
...prev,
[columnKey]: currentWidth
}))
}
const handleMouseUp = () => {
const nextColumns = resolvedColumns.map((column) =>
column.key === columnKey
? { ...column, width: currentWidth }
: column
)
setWidthOverrides({})
setResizingKey(null)
persistColumns(nextColumns)
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.body.classList.remove('objectKanbanColumnResizing')
}
document.body.classList.add('objectKanbanColumnResizing')
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
},
[displayColumns, persistColumns, resolvedColumns]
)
if (!categoryProperty) {
return (
<Flex align='center' justify='center'>
<MissingPlaceholder
message='Select a category property to continue.'
hasBackground={true}
hasBorder={false}
height='260px'
/>
</Flex>
)
}
if (hasLoaded && categoryValues.length === 0) {
return (
<MissingPlaceholder
message='No data.'
hasBackground={true}
hasBorder={false}
height='260px'
/>
)
}
const showSkeleton = !hasLoaded
return (
<div className='objectKanbanContainerWrapper'>
<div className='objectKanbanOverlayColumns'>
<div className='objectKanbanOverlayTrack' ref={overlayTrackRef}>
<Flex gap='middle' className='objectKanbanOverlayRow'>
{displayColumns.map(({ key: columnKey, width }, columnIndex) => (
<ObjectKanbanColumnResizeOverlay
key={columnKey}
columnKey={columnKey}
width={width}
columnIndex={columnIndex}
isResizing={resizingKey === columnKey}
onResizeStart={handleColumnResizeStart}
/>
))}
</Flex>
</div>
</div>
<Spin spinning={showSkeleton}>
<div className='objectKanbanContainerSkeletons'>
<ObjectKanbanHeader
columns={displayColumns}
categoryProperty={categoryProperty}
categoryPropertyDef={categoryPropertyDef}
trackRef={headerTrackRef}
skeletonColumnCount={
showSkeleton ? skeletonLayout.columnCount : 0
}
lazyLoading={showSkeleton || lazyLoading}
loadingColumnKeys={reloadingColumnKeys}
draggedKey={draggedKey}
dropTargetKey={dropTargetKey}
onColumnDragStart={handleColumnDragStart}
onColumnDragOver={handleColumnDragOver}
onColumnDrop={handleColumnDrop}
onColumnDragEnd={handleColumnDragEnd}
/>
<div className='objectKanbanKanbanBody' ref={kanbanBodyRef}>
{!showSkeleton && (
<div className='objectKanbanContainer'>
<ScrollBox
className='objectKanbanScroll'
scrollableNodeProps={{ ref: setScrollContainerRef }}
>
<Flex gap='middle' className='objectKanban'>
{displayColumns.map(
({ key: columnKey, value: categoryValue, width }) => (
<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}
width={width}
/>
)
)}
</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'>
{displayColumns.map(({ key: columnKey, width }) => (
<ObjectKanbanColumnSkeleton
key={columnKey}
columnKey={columnKey}
width={width}
model={model}
modelProperties={modelProperties}
visibleColumns={visibleColumns}
isResizing={resizingKey === columnKey}
onResizeStart={handleColumnResizeStart}
/>
))}
</Flex>
</div>
</div>
)}
</div>
</div>
</Spin>
</div>
)
}
)
ObjectKanban.displayName = 'ObjectKanban'
ObjectKanban.propTypes = {
type: PropTypes.string.isRequired,
categoryProperty: PropTypes.string,
baseFilter: PropTypes.object,
masterFilter: PropTypes.object,
sorter: PropTypes.object,
lazyLoading: PropTypes.bool,
pageSize: PropTypes.number,
model: PropTypes.object.isRequired,
modelProperties: PropTypes.array.isRequired,
visibleColumns: PropTypes.object,
isEditing: PropTypes.bool,
rowActions: PropTypes.array,
renderActions: PropTypes.func,
columns: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
width: PropTypes.number
})
),
onColumnsChange: PropTypes.func
}
export default ObjectKanban