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

View File

@ -11,6 +11,7 @@ import {
} from 'react' } from 'react'
import { Empty, Flex } from 'antd' import { Empty, Flex } from 'antd'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classNames from 'classnames'
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'
@ -97,6 +98,99 @@ const getKanbanSkeletonLayout = (width, height) => {
return { columnCount, cardCount } 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 = ({ const ObjectKanbanSkeletonColumns = ({
columnCount, columnCount,
cardCount = 0, cardCount = 0,
@ -105,7 +199,9 @@ const ObjectKanbanSkeletonColumns = ({
modelProperties, modelProperties,
visibleColumns, visibleColumns,
keyPrefix = 'skeleton', keyPrefix = 'skeleton',
columns = null columns = null,
resizingKey = null,
onColumnResizeStart
}) => { }) => {
const skeletonColumns = const skeletonColumns =
columns?.length > 0 columns?.length > 0
@ -120,45 +216,34 @@ const ObjectKanbanSkeletonColumns = ({
<div className='objectKanbanSkeletonTrack' ref={trackRef}> <div className='objectKanbanSkeletonTrack' ref={trackRef}>
<Flex gap='middle' className='objectKanbanSkeletonRow'> <Flex gap='middle' className='objectKanbanSkeletonRow'>
{skeletonColumns.map((column, columnIndex) => ( {skeletonColumns.map((column, columnIndex) => (
<div <ObjectKanbanColumnSkeleton
key={column.key || `${keyPrefix}-${columnIndex}`} key={column.key || `${keyPrefix}-${columnIndex}`}
className='objectKanbanColumnSkeleton' columnKey={column.key}
style={{ width={column.width}
width: column.width, cardCount={cardCount}
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} model={model}
modelProperties={modelProperties} modelProperties={modelProperties}
visibleColumns={visibleColumns} visibleColumns={visibleColumns}
record={{ keyPrefix={keyPrefix}
_id: `${keyPrefix}-${columnIndex}-${cardIndex}` columnIndex={columnIndex}
}} isResizing={resizingKey === column.key}
cardStyle='bordered' onResizeStart={onColumnResizeStart}
/> />
))} ))}
</Flex> </Flex>
)}
</div>
</div>
))}
</Flex>
</div> </div>
</div> </div>
) )
} }
ObjectKanbanColumnResizeOverlay.propTypes = {
columnKey: PropTypes.string,
width: PropTypes.number.isRequired,
columnIndex: PropTypes.number,
isResizing: PropTypes.bool,
onResizeStart: PropTypes.func
}
ObjectKanbanSkeletonColumns.propTypes = { ObjectKanbanSkeletonColumns.propTypes = {
columnCount: PropTypes.number.isRequired, columnCount: PropTypes.number.isRequired,
cardCount: PropTypes.number, cardCount: PropTypes.number,
@ -167,7 +252,9 @@ ObjectKanbanSkeletonColumns.propTypes = {
modelProperties: PropTypes.array.isRequired, modelProperties: PropTypes.array.isRequired,
visibleColumns: PropTypes.object, visibleColumns: PropTypes.object,
keyPrefix: PropTypes.string, keyPrefix: PropTypes.string,
columns: PropTypes.array columns: PropTypes.array,
resizingKey: PropTypes.string,
onColumnResizeStart: PropTypes.func
} }
const ObjectKanban = forwardRef( const ObjectKanban = forwardRef(
@ -200,6 +287,7 @@ const ObjectKanban = forwardRef(
const columnRefs = useRef({}) const columnRefs = useRef({})
const headerTrackRef = useRef(null) const headerTrackRef = useRef(null)
const skeletonTrackRef = useRef(null) const skeletonTrackRef = useRef(null)
const overlayTrackRef = useRef(null)
const kanbanBodyRef = useRef(null) const kanbanBodyRef = useRef(null)
const [scrollElement, setScrollElement] = useState(null) const [scrollElement, setScrollElement] = useState(null)
const [categoryValues, setCategoryValues] = useState([]) const [categoryValues, setCategoryValues] = useState([])
@ -272,6 +360,7 @@ const ObjectKanban = forwardRef(
const mainScroll = scrollElement const mainScroll = scrollElement
const headerTrack = headerTrackRef.current const headerTrack = headerTrackRef.current
const skeletonTrack = skeletonTrackRef.current const skeletonTrack = skeletonTrackRef.current
const overlayTrack = overlayTrackRef.current
if (!mainScroll || !headerTrack) return undefined if (!mainScroll || !headerTrack) return undefined
const handleMainScroll = () => { const handleMainScroll = () => {
@ -280,6 +369,9 @@ const ObjectKanban = forwardRef(
if (skeletonTrack) { if (skeletonTrack) {
skeletonTrack.style.transform = offset skeletonTrack.style.transform = offset
} }
if (overlayTrack) {
overlayTrack.style.transform = offset
}
} }
handleMainScroll() handleMainScroll()
@ -429,12 +521,14 @@ const ObjectKanban = forwardRef(
setReloadingColumnKeys(previousKeys) setReloadingColumnKeys(previousKeys)
try { try {
lastCategoryQueryKeyRef.current = null lastCategoryQueryKeyRef.current = null
const nextValues = await load(baseFilterRef.current, sorterRef.current, { const nextValues = await load(
baseFilterRef.current,
sorterRef.current,
{
silent: true silent: true
}) }
const nextKeySet = new Set(
(nextValues || []).map(getCategoryValueKey)
) )
const nextKeySet = new Set((nextValues || []).map(getCategoryValueKey))
// Only reload columns that existed before; newly created columns // Only reload columns that existed before; newly created columns
// mount and load themselves. // mount and load themselves.
const keysToReload = previousKeys.filter((key) => nextKeySet.has(key)) const keysToReload = previousKeys.filter((key) => nextKeySet.has(key))
@ -480,9 +574,9 @@ const ObjectKanban = forwardRef(
updatedData, updatedData,
categoryPropertyName categoryPropertyName
) )
? getCategoryColumnKeys( ? getCategoryColumnKeys(updatedData[categoryPropertyName]).filter(
updatedData[categoryPropertyName] (key) => previousKeys.has(key)
).filter((key) => previousKeys.has(key)) )
: [] : []
const tentativeKeys = [...new Set([...oldKeys, ...updatedKeys])] const tentativeKeys = [...new Set([...oldKeys, ...updatedKeys])]
@ -755,11 +849,7 @@ const ObjectKanban = forwardRef(
setDropTargetKey(null) setDropTargetKey(null)
return return
} }
const nextColumns = reorderColumnsByKey( const nextColumns = reorderColumnsByKey(resolvedColumns, fromKey, toKey)
resolvedColumns,
fromKey,
toKey
)
persistColumns(nextColumns) persistColumns(nextColumns)
setDraggedKey(null) setDraggedKey(null)
setDropTargetKey(null) setDropTargetKey(null)
@ -845,6 +935,23 @@ const ObjectKanban = forwardRef(
return ( return (
<div className='objectKanbanContainerWrapper'> <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}> <Spin spinning={showSkeleton}>
<div className='objectKanbanContainerSkeletons'> <div className='objectKanbanContainerSkeletons'>
<ObjectKanbanHeader <ObjectKanbanHeader
@ -898,10 +1005,6 @@ const ObjectKanban = forwardRef(
rowActions={rowActions} rowActions={rowActions}
renderActions={renderActions} renderActions={renderActions}
width={width} width={width}
isResizing={resizingKey === columnKey}
onResizeStart={(event) =>
handleColumnResizeStart(event, columnKey)
}
/> />
) )
)} )}
@ -926,16 +1029,16 @@ const ObjectKanban = forwardRef(
> >
<Flex gap='middle' className='objectKanbanSkeletonRow'> <Flex gap='middle' className='objectKanbanSkeletonRow'>
{displayColumns.map(({ key: columnKey, width }) => ( {displayColumns.map(({ key: columnKey, width }) => (
<div <ObjectKanbanColumnSkeleton
key={columnKey} key={columnKey}
className='objectKanbanColumnSkeleton' columnKey={columnKey}
style={{ width={width}
width, model={model}
flex: `0 0 ${width}px` modelProperties={modelProperties}
}} visibleColumns={visibleColumns}
> isResizing={resizingKey === columnKey}
<div className='objectKanbanColumnCards' /> onResizeStart={handleColumnResizeStart}
</div> />
))} ))}
</Flex> </Flex>
</div> </div>

View File

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