Enhance ObjectKanban Component with Column Resizing Overlays and Improved Skeleton Structure
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced ObjectKanbanColumnResizeHandle and ObjectKanbanColumnResizeOverlay components for better column resizing functionality.
- Updated CSS styles to support new overlays and improve visual feedback during resizing actions.
- Refactored ObjectKanbanSkeletonColumns to utilize the new skeleton structure for enhanced layout and responsiveness.
- Adjusted properties and styles for better content visibility and interaction during resizing.
This commit is contained in:
Tom Butcher 2026-09-03 15:09:27 +01:00
parent d62431c4fa
commit c75876ab3c
3 changed files with 187 additions and 84 deletions

View File

@ -1451,11 +1451,20 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
.objectKanbanContainerSkeletons {
display: flex;
flex-direction: column;
position: relative;
flex: 1;
min-width: 0;
min-height: 0;
}
.objectKanbanOverlayColumns {
position: absolute;
inset: 0;
z-index: 4;
overflow: hidden;
pointer-events: none;
}
.objectKanbanKanbanBody {
position: relative;
flex: 1;
@ -1568,14 +1577,16 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
pointer-events: none;
}
.objectKanbanSkeletonTrack {
.objectKanbanSkeletonTrack,
.objectKanbanOverlayTrack {
width: max-content;
min-width: 100%;
height: 100%;
will-change: transform;
}
.objectKanbanSkeletonRow {
.objectKanbanSkeletonRow,
.objectKanbanOverlayRow {
width: max-content;
min-width: 100%;
height: 100%;
@ -1592,16 +1603,21 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
border-radius: 0 0 12px 12px;
}
.objectKanbanColumnResizeOverlay {
position: relative;
}
.objectKanbanColumnResizeHandle {
position: absolute;
top: 0;
right: -10px;
right: -11px;
bottom: 0;
width: 11px;
z-index: 3;
cursor: col-resize;
touch-action: none;
user-select: none;
pointer-events: auto;
}
.objectKanbanColumnResizeHandle::before {
@ -1609,7 +1625,7 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
position: absolute;
top: 11px;
bottom: 11px;
right: 0;
right: 1px;
width: 2px;
transform: translateX(-50%);
opacity: 0.6;
@ -1620,7 +1636,7 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
content: '';
position: absolute;
top: 50%;
right: 0;
right: 1px;
width: 2px;
height: 20px;
transform: translate(-50%, -50%);
@ -1648,7 +1664,7 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
opacity: 1;
}
.objectKanbanColumn:hover .objectKanbanColumnResizeHandle::before {
.objectKanbanColumnResizeHandle:hover::before {
background: #0e3a5b;
}
@ -1660,7 +1676,7 @@ body.objectKanbanColumnResizing * {
.objectKanbanColumnSkeleton {
border-radius: 0 0 12px 12px;
overflow: hidden;
overflow: visible;
}
.objectKanbanColumnCards {

View File

@ -11,6 +11,7 @@ import {
} from 'react'
import { Empty, Flex } from 'antd'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { ApiServerContext } from '../context/ApiServerContext'
import ObjectCard from './ObjectCard'
import ObjectKanbanColumn from './ObjectKanbanColumn'
@ -97,6 +98,99 @@ const getKanbanSkeletonLayout = (width, 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,
@ -105,7 +199,9 @@ const ObjectKanbanSkeletonColumns = ({
modelProperties,
visibleColumns,
keyPrefix = 'skeleton',
columns = null
columns = null,
resizingKey = null,
onColumnResizeStart
}) => {
const skeletonColumns =
columns?.length > 0
@ -120,38 +216,19 @@ const ObjectKanbanSkeletonColumns = ({
<div className='objectKanbanSkeletonTrack' ref={trackRef}>
<Flex gap='middle' className='objectKanbanSkeletonRow'>
{skeletonColumns.map((column, columnIndex) => (
<div
<ObjectKanbanColumnSkeleton
key={column.key || `${keyPrefix}-${columnIndex}`}
className='objectKanbanColumnSkeleton'
style={{
width: column.width,
flex: `0 0 ${column.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}-${columnIndex}-${cardIndex}`
}}
cardStyle='bordered'
/>
))}
</Flex>
)}
</div>
</div>
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>
@ -159,6 +236,14 @@ const ObjectKanbanSkeletonColumns = ({
)
}
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,
@ -167,7 +252,9 @@ ObjectKanbanSkeletonColumns.propTypes = {
modelProperties: PropTypes.array.isRequired,
visibleColumns: PropTypes.object,
keyPrefix: PropTypes.string,
columns: PropTypes.array
columns: PropTypes.array,
resizingKey: PropTypes.string,
onColumnResizeStart: PropTypes.func
}
const ObjectKanban = forwardRef(
@ -200,6 +287,7 @@ const ObjectKanban = forwardRef(
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([])
@ -272,6 +360,7 @@ const ObjectKanban = forwardRef(
const mainScroll = scrollElement
const headerTrack = headerTrackRef.current
const skeletonTrack = skeletonTrackRef.current
const overlayTrack = overlayTrackRef.current
if (!mainScroll || !headerTrack) return undefined
const handleMainScroll = () => {
@ -280,6 +369,9 @@ const ObjectKanban = forwardRef(
if (skeletonTrack) {
skeletonTrack.style.transform = offset
}
if (overlayTrack) {
overlayTrack.style.transform = offset
}
}
handleMainScroll()
@ -429,12 +521,14 @@ const ObjectKanban = forwardRef(
setReloadingColumnKeys(previousKeys)
try {
lastCategoryQueryKeyRef.current = null
const nextValues = await load(baseFilterRef.current, sorterRef.current, {
silent: true
})
const nextKeySet = new Set(
(nextValues || []).map(getCategoryValueKey)
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))
@ -480,9 +574,9 @@ const ObjectKanban = forwardRef(
updatedData,
categoryPropertyName
)
? getCategoryColumnKeys(
updatedData[categoryPropertyName]
).filter((key) => previousKeys.has(key))
? getCategoryColumnKeys(updatedData[categoryPropertyName]).filter(
(key) => previousKeys.has(key)
)
: []
const tentativeKeys = [...new Set([...oldKeys, ...updatedKeys])]
@ -755,11 +849,7 @@ const ObjectKanban = forwardRef(
setDropTargetKey(null)
return
}
const nextColumns = reorderColumnsByKey(
resolvedColumns,
fromKey,
toKey
)
const nextColumns = reorderColumnsByKey(resolvedColumns, fromKey, toKey)
persistColumns(nextColumns)
setDraggedKey(null)
setDropTargetKey(null)
@ -845,6 +935,23 @@ const ObjectKanban = forwardRef(
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
@ -898,10 +1005,6 @@ const ObjectKanban = forwardRef(
rowActions={rowActions}
renderActions={renderActions}
width={width}
isResizing={resizingKey === columnKey}
onResizeStart={(event) =>
handleColumnResizeStart(event, columnKey)
}
/>
)
)}
@ -926,16 +1029,16 @@ const ObjectKanban = forwardRef(
>
<Flex gap='middle' className='objectKanbanSkeletonRow'>
{displayColumns.map(({ key: columnKey, width }) => (
<div
<ObjectKanbanColumnSkeleton
key={columnKey}
className='objectKanbanColumnSkeleton'
style={{
width,
flex: `0 0 ${width}px`
}}
>
<div className='objectKanbanColumnCards' />
</div>
columnKey={columnKey}
width={width}
model={model}
modelProperties={modelProperties}
visibleColumns={visibleColumns}
isResizing={resizingKey === columnKey}
onResizeStart={handleColumnResizeStart}
/>
))}
</Flex>
</div>

View File

@ -11,7 +11,6 @@ import {
} from 'react'
import { Flex } from 'antd'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { ApiServerContext } from '../context/ApiServerContext'
import ObjectCard from './ObjectCard'
import Spin from './Spin'
@ -42,9 +41,7 @@ const ObjectKanbanColumn = forwardRef(
lazyLoading = false,
renderActions,
scrollElement = null,
width,
isResizing = false,
onResizeStart
width
},
ref
) => {
@ -597,17 +594,6 @@ const ObjectKanbanColumn = forwardRef(
</Flex>
</Spin>
</div>
{onResizeStart && (
<div
className={classNames('objectKanbanColumnResizeHandle', {
active: isResizing
})}
role='separator'
aria-orientation='vertical'
aria-label='Resize column'
onMouseDown={(event) => onResizeStart(event)}
/>
)}
</Flex>
)
}
@ -631,9 +617,7 @@ ObjectKanbanColumn.propTypes = {
lazyLoading: PropTypes.bool,
renderActions: PropTypes.func,
scrollElement: PropTypes.object,
width: PropTypes.number,
isResizing: PropTypes.bool,
onResizeStart: PropTypes.func
width: PropTypes.number
}
export default ObjectKanbanColumn