Enhance ObjectKanban Component with Column Resizing Overlays and Improved Skeleton Structure
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
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:
parent
d62431c4fa
commit
c75876ab3c
@ -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 {
|
||||
|
||||
@ -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,45 +216,34 @@ 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
|
||||
columnKey={column.key}
|
||||
width={column.width}
|
||||
cardCount={cardCount}
|
||||
model={model}
|
||||
modelProperties={modelProperties}
|
||||
visibleColumns={visibleColumns}
|
||||
record={{
|
||||
_id: `${keyPrefix}-${columnIndex}-${cardIndex}`
|
||||
}}
|
||||
cardStyle='bordered'
|
||||
keyPrefix={keyPrefix}
|
||||
columnIndex={columnIndex}
|
||||
isResizing={resizingKey === column.key}
|
||||
onResizeStart={onColumnResizeStart}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</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,
|
||||
@ -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, {
|
||||
const nextValues = await load(
|
||||
baseFilterRef.current,
|
||||
sorterRef.current,
|
||||
{
|
||||
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
|
||||
// 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>
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user