Enhance ObjectKanban and ObjectTable Components with Column Resizing and Drag-and-Drop Functionality
- Implemented dynamic column resizing in ObjectKanban and ObjectKanbanColumn, allowing users to adjust column widths for better layout control. - Added drag-and-drop support for reordering columns in ObjectKanbanHeader, improving user interaction and customization of the kanban view. - Updated CSS styles in App.css to accommodate new resizing handles and visual feedback during drag-and-drop actions. - Refactored ObjectKanban and related components to manage column states and widths effectively, ensuring a responsive and user-friendly experience.
This commit is contained in:
parent
65e25ae077
commit
5e6b4213ab
@ -1520,14 +1520,35 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.objectKanbanHeaderCell {
|
.objectKanbanHeaderCell {
|
||||||
flex: 0 0 360px;
|
flex: 0 0 auto;
|
||||||
min-width: 360px;
|
min-width: 200px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
border: 1px solid var(--ant-color-border-secondary);
|
border: 1px solid var(--ant-color-border-secondary);
|
||||||
border-bottom: 1px solid var(--color-descriptions-border);
|
border-bottom: 1px solid var(--color-descriptions-border);
|
||||||
border-radius: 12px 12px 0 0;
|
border-radius: 12px 12px 0 0;
|
||||||
background-color: var(--layout-header-bg);
|
background-color: var(--layout-header-bg);
|
||||||
|
transition:
|
||||||
|
outline-color 0.15s ease,
|
||||||
|
opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectKanbanHeaderCell--dragging {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectKanbanHeaderCell--dropTarget {
|
||||||
|
outline: 2px dashed color-mix(in srgb, var(--color-primary) 55%, transparent);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectKanbanHeaderCellActions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectKanbanHeaderDragHandle {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.objectKanban {
|
.objectKanban {
|
||||||
@ -1563,13 +1584,80 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
|
|
||||||
.objectKanbanColumn,
|
.objectKanbanColumn,
|
||||||
.objectKanbanColumnSkeleton {
|
.objectKanbanColumnSkeleton {
|
||||||
flex: 0 0 360px;
|
position: relative;
|
||||||
min-width: 360px;
|
flex: 0 0 auto;
|
||||||
|
min-width: 200px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-radius: 0 0 12px 12px;
|
border-radius: 0 0 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.objectKanbanColumnResizeHandle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -10px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 11px;
|
||||||
|
z-index: 3;
|
||||||
|
cursor: col-resize;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectKanbanColumnResizeHandle::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 11px;
|
||||||
|
bottom: 11px;
|
||||||
|
right: 0;
|
||||||
|
width: 2px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectKanbanColumnResizeHandle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
width: 2px;
|
||||||
|
height: 20px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectKanbanColumnResizeHandle.active::before,
|
||||||
|
.objectKanbanColumnResizeHandle.active::after {
|
||||||
|
background: #0e3a5b;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .objectKanbanColumnResizeHandle::after {
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .objectKanbanColumnResizeHandle::before {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .objectKanbanColumnResizeHandle.active::before,
|
||||||
|
.dark-mode .objectKanbanColumnResizeHandle.active::after {
|
||||||
|
background: #0b4c7e;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectKanbanColumn:hover .objectKanbanColumnResizeHandle::before {
|
||||||
|
background: #0e3a5b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.objectKanbanColumnResizing,
|
||||||
|
body.objectKanbanColumnResizing * {
|
||||||
|
cursor: col-resize !important;
|
||||||
|
user-select: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.objectKanbanColumnSkeleton {
|
.objectKanbanColumnSkeleton {
|
||||||
border-radius: 0 0 12px 12px;
|
border-radius: 0 0 12px 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -98,7 +98,7 @@ const ObjectCard = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
styles={{ body: { padding: 18 } }}
|
styles={{ body: { padding: '14px 16px' } }}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
variant={cardStyle}
|
variant={cardStyle}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -17,13 +17,31 @@ import ObjectKanbanColumn from './ObjectKanbanColumn'
|
|||||||
import ObjectKanbanHeader from './ObjectKanbanHeader'
|
import ObjectKanbanHeader from './ObjectKanbanHeader'
|
||||||
import ScrollBox from './ScrollBox'
|
import ScrollBox from './ScrollBox'
|
||||||
import Spin from './Spin'
|
import Spin from './Spin'
|
||||||
import { getCategoryColumnKeys, getCategoryValueKey } from './viewModeUtils'
|
import {
|
||||||
|
getCategoryColumnKeys,
|
||||||
|
getCategoryValueKey,
|
||||||
|
KANBAN_DEFAULT_COLUMN_WIDTH,
|
||||||
|
KANBAN_MIN_COLUMN_WIDTH,
|
||||||
|
resolveKanbanColumns,
|
||||||
|
toKanbanColumnsConfig
|
||||||
|
} from './viewModeUtils'
|
||||||
import { areValuesEqual } from '../utils/Utils'
|
import { areValuesEqual } from '../utils/Utils'
|
||||||
|
|
||||||
const KANBAN_COLUMN_WIDTH = 360
|
const KANBAN_COLUMN_WIDTH = KANBAN_DEFAULT_COLUMN_WIDTH
|
||||||
const KANBAN_GAP = 16
|
const KANBAN_GAP = 16
|
||||||
const KANBAN_CARD_MIN_HEIGHT = 136
|
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) => {
|
const getUpdateKeys = (updated) => {
|
||||||
if (!updated || typeof updated !== 'object') return []
|
if (!updated || typeof updated !== 'object') return []
|
||||||
return Object.keys(updated).filter(
|
return Object.keys(updated).filter(
|
||||||
@ -86,45 +104,60 @@ const ObjectKanbanSkeletonColumns = ({
|
|||||||
model,
|
model,
|
||||||
modelProperties,
|
modelProperties,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
keyPrefix = 'skeleton'
|
keyPrefix = 'skeleton',
|
||||||
}) => (
|
columns = null
|
||||||
<div className='objectKanbanSkeletonColumns objectKanbanSkeletonColumns--loading'>
|
}) => {
|
||||||
<div className='objectKanbanSkeletonTrack' ref={trackRef}>
|
const skeletonColumns =
|
||||||
<Flex gap='middle' className='objectKanbanSkeletonRow'>
|
columns?.length > 0
|
||||||
{Array.from({ length: columnCount }).map((_, columnIndex) => (
|
? columns
|
||||||
<div
|
: Array.from({ length: columnCount }).map((_, columnIndex) => ({
|
||||||
key={`${keyPrefix}-${columnIndex}`}
|
key: `${keyPrefix}-${columnIndex}`,
|
||||||
className='objectKanbanColumnSkeleton'
|
width: KANBAN_COLUMN_WIDTH
|
||||||
>
|
}))
|
||||||
<div className='objectKanbanColumnCards'>
|
|
||||||
{cardCount > 0 && (
|
return (
|
||||||
<Flex
|
<div className='objectKanbanSkeletonColumns objectKanbanSkeletonColumns--loading'>
|
||||||
vertical
|
<div className='objectKanbanSkeletonTrack' ref={trackRef}>
|
||||||
gap='middle'
|
<Flex gap='middle' className='objectKanbanSkeletonRow'>
|
||||||
className='objectKanbanColumnCardsInner'
|
{skeletonColumns.map((column, columnIndex) => (
|
||||||
>
|
<div
|
||||||
{Array.from({ length: cardCount }).map((_, cardIndex) => (
|
key={column.key || `${keyPrefix}-${columnIndex}`}
|
||||||
<ObjectCard
|
className='objectKanbanColumnSkeleton'
|
||||||
key={cardIndex}
|
style={{
|
||||||
isSkeleton
|
width: column.width,
|
||||||
model={model}
|
flex: `0 0 ${column.width}px`
|
||||||
modelProperties={modelProperties}
|
}}
|
||||||
visibleColumns={visibleColumns}
|
>
|
||||||
record={{
|
<div className='objectKanbanColumnCards'>
|
||||||
_id: `${keyPrefix}-${columnIndex}-${cardIndex}`
|
{cardCount > 0 && (
|
||||||
}}
|
<Flex
|
||||||
cardStyle='bordered'
|
vertical
|
||||||
/>
|
gap='middle'
|
||||||
))}
|
className='objectKanbanColumnCardsInner'
|
||||||
</Flex>
|
>
|
||||||
)}
|
{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>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</Flex>
|
||||||
</Flex>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
ObjectKanbanSkeletonColumns.propTypes = {
|
ObjectKanbanSkeletonColumns.propTypes = {
|
||||||
columnCount: PropTypes.number.isRequired,
|
columnCount: PropTypes.number.isRequired,
|
||||||
@ -133,7 +166,8 @@ ObjectKanbanSkeletonColumns.propTypes = {
|
|||||||
model: PropTypes.object.isRequired,
|
model: PropTypes.object.isRequired,
|
||||||
modelProperties: PropTypes.array.isRequired,
|
modelProperties: PropTypes.array.isRequired,
|
||||||
visibleColumns: PropTypes.object,
|
visibleColumns: PropTypes.object,
|
||||||
keyPrefix: PropTypes.string
|
keyPrefix: PropTypes.string,
|
||||||
|
columns: PropTypes.array
|
||||||
}
|
}
|
||||||
|
|
||||||
const ObjectKanban = forwardRef(
|
const ObjectKanban = forwardRef(
|
||||||
@ -151,7 +185,9 @@ const ObjectKanban = forwardRef(
|
|||||||
visibleColumns = {},
|
visibleColumns = {},
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
rowActions = [],
|
rowActions = [],
|
||||||
renderActions
|
renderActions,
|
||||||
|
columns: storedColumns = [],
|
||||||
|
onColumnsChange
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@ -175,6 +211,11 @@ const ObjectKanban = forwardRef(
|
|||||||
cardCount: 3
|
cardCount: 3
|
||||||
})
|
})
|
||||||
const [reloadingColumnKeys, setReloadingColumnKeys] = useState([])
|
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 baseFilterRef = useRef(baseFilter)
|
||||||
const masterFilterRef = useRef(masterFilter)
|
const masterFilterRef = useRef(masterFilter)
|
||||||
@ -622,6 +663,168 @@ const ObjectKanban = forwardRef(
|
|||||||
[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) {
|
if (!categoryProperty) {
|
||||||
return (
|
return (
|
||||||
<Flex align='center' justify='center' style={{ height: '100%' }}>
|
<Flex align='center' justify='center' style={{ height: '100%' }}>
|
||||||
@ -645,7 +848,7 @@ const ObjectKanban = forwardRef(
|
|||||||
<Spin spinning={showSkeleton}>
|
<Spin spinning={showSkeleton}>
|
||||||
<div className='objectKanbanContainerSkeletons'>
|
<div className='objectKanbanContainerSkeletons'>
|
||||||
<ObjectKanbanHeader
|
<ObjectKanbanHeader
|
||||||
categoryValues={categoryValues}
|
columns={displayColumns}
|
||||||
categoryProperty={categoryProperty}
|
categoryProperty={categoryProperty}
|
||||||
categoryPropertyDef={categoryPropertyDef}
|
categoryPropertyDef={categoryPropertyDef}
|
||||||
trackRef={headerTrackRef}
|
trackRef={headerTrackRef}
|
||||||
@ -654,6 +857,12 @@ const ObjectKanban = forwardRef(
|
|||||||
}
|
}
|
||||||
lazyLoading={showSkeleton || lazyLoading}
|
lazyLoading={showSkeleton || lazyLoading}
|
||||||
loadingColumnKeys={reloadingColumnKeys}
|
loadingColumnKeys={reloadingColumnKeys}
|
||||||
|
draggedKey={draggedKey}
|
||||||
|
dropTargetKey={dropTargetKey}
|
||||||
|
onColumnDragStart={handleColumnDragStart}
|
||||||
|
onColumnDragOver={handleColumnDragOver}
|
||||||
|
onColumnDrop={handleColumnDrop}
|
||||||
|
onColumnDragEnd={handleColumnDragEnd}
|
||||||
/>
|
/>
|
||||||
<div className='objectKanbanKanbanBody' ref={kanbanBodyRef}>
|
<div className='objectKanbanKanbanBody' ref={kanbanBodyRef}>
|
||||||
{!showSkeleton && (
|
{!showSkeleton && (
|
||||||
@ -663,9 +872,8 @@ const ObjectKanban = forwardRef(
|
|||||||
scrollableNodeProps={{ ref: setScrollContainerRef }}
|
scrollableNodeProps={{ ref: setScrollContainerRef }}
|
||||||
>
|
>
|
||||||
<Flex gap='middle' className='objectKanban'>
|
<Flex gap='middle' className='objectKanban'>
|
||||||
{categoryValues.map((categoryValue) => {
|
{displayColumns.map(
|
||||||
const columnKey = getCategoryValueKey(categoryValue)
|
({ key: columnKey, value: categoryValue, width }) => (
|
||||||
return (
|
|
||||||
<ObjectKanbanColumn
|
<ObjectKanbanColumn
|
||||||
key={columnKey}
|
key={columnKey}
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
@ -689,9 +897,14 @@ const ObjectKanban = forwardRef(
|
|||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
rowActions={rowActions}
|
rowActions={rowActions}
|
||||||
renderActions={renderActions}
|
renderActions={renderActions}
|
||||||
|
width={width}
|
||||||
|
isResizing={resizingKey === columnKey}
|
||||||
|
onResizeStart={(event) =>
|
||||||
|
handleColumnResizeStart(event, columnKey)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</ScrollBox>
|
</ScrollBox>
|
||||||
</div>
|
</div>
|
||||||
@ -712,17 +925,18 @@ const ObjectKanban = forwardRef(
|
|||||||
ref={skeletonTrackRef}
|
ref={skeletonTrackRef}
|
||||||
>
|
>
|
||||||
<Flex gap='middle' className='objectKanbanSkeletonRow'>
|
<Flex gap='middle' className='objectKanbanSkeletonRow'>
|
||||||
{categoryValues.map((categoryValue) => {
|
{displayColumns.map(({ key: columnKey, width }) => (
|
||||||
const columnKey = getCategoryValueKey(categoryValue)
|
<div
|
||||||
return (
|
key={columnKey}
|
||||||
<div
|
className='objectKanbanColumnSkeleton'
|
||||||
key={columnKey}
|
style={{
|
||||||
className='objectKanbanColumnSkeleton'
|
width,
|
||||||
>
|
flex: `0 0 ${width}px`
|
||||||
<div className='objectKanbanColumnCards' />
|
}}
|
||||||
</div>
|
>
|
||||||
)
|
<div className='objectKanbanColumnCards' />
|
||||||
})}
|
</div>
|
||||||
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -750,7 +964,14 @@ ObjectKanban.propTypes = {
|
|||||||
visibleColumns: PropTypes.object,
|
visibleColumns: PropTypes.object,
|
||||||
isEditing: PropTypes.bool,
|
isEditing: PropTypes.bool,
|
||||||
rowActions: PropTypes.array,
|
rowActions: PropTypes.array,
|
||||||
renderActions: PropTypes.func
|
renderActions: PropTypes.func,
|
||||||
|
columns: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
|
width: PropTypes.number
|
||||||
|
})
|
||||||
|
),
|
||||||
|
onColumnsChange: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectKanban
|
export default ObjectKanban
|
||||||
|
|||||||
@ -11,10 +11,11 @@ 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'
|
||||||
import { toCategoryFilterValue } from './viewModeUtils'
|
import { KANBAN_MIN_COLUMN_WIDTH, toCategoryFilterValue } from './viewModeUtils'
|
||||||
|
|
||||||
const SCROLL_THRESHOLD = 50
|
const SCROLL_THRESHOLD = 50
|
||||||
|
|
||||||
@ -40,7 +41,10 @@ const ObjectKanbanColumn = forwardRef(
|
|||||||
rowActions = [],
|
rowActions = [],
|
||||||
lazyLoading = false,
|
lazyLoading = false,
|
||||||
renderActions,
|
renderActions,
|
||||||
scrollElement = null
|
scrollElement = null,
|
||||||
|
width,
|
||||||
|
isResizing = false,
|
||||||
|
onResizeStart
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@ -545,8 +549,13 @@ const ObjectKanbanColumn = forwardRef(
|
|||||||
tableData
|
tableData
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const columnStyle =
|
||||||
|
typeof width === 'number'
|
||||||
|
? { width, flex: `0 0 ${width}px`, minWidth: KANBAN_MIN_COLUMN_WIDTH }
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex vertical className='objectKanbanColumn'>
|
<Flex vertical className='objectKanbanColumn' style={columnStyle}>
|
||||||
<div className='objectKanbanColumnCards' ref={containerRef}>
|
<div className='objectKanbanColumnCards' ref={containerRef}>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<Flex
|
<Flex
|
||||||
@ -588,6 +597,17 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -610,7 +630,10 @@ ObjectKanbanColumn.propTypes = {
|
|||||||
rowActions: PropTypes.array,
|
rowActions: PropTypes.array,
|
||||||
lazyLoading: PropTypes.bool,
|
lazyLoading: PropTypes.bool,
|
||||||
renderActions: PropTypes.func,
|
renderActions: PropTypes.func,
|
||||||
scrollElement: PropTypes.object
|
scrollElement: PropTypes.object,
|
||||||
|
width: PropTypes.number,
|
||||||
|
isResizing: PropTypes.bool,
|
||||||
|
onResizeStart: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectKanbanColumn
|
export default ObjectKanbanColumn
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
import { Flex, Skeleton } from 'antd'
|
import { Flex, Skeleton } from 'antd'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import { HolderOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
import { getCategoryValueKey } from './viewModeUtils'
|
import { KANBAN_DEFAULT_COLUMN_WIDTH } from './viewModeUtils'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
|
||||||
|
|
||||||
const ObjectKanbanHeader = ({
|
const ObjectKanbanHeader = ({
|
||||||
categoryValues,
|
columns = [],
|
||||||
categoryProperty,
|
categoryProperty,
|
||||||
categoryPropertyDef,
|
categoryPropertyDef,
|
||||||
trackRef,
|
trackRef,
|
||||||
skeletonColumnCount = 0,
|
skeletonColumnCount = 0,
|
||||||
lazyLoading = false,
|
lazyLoading = false,
|
||||||
loadingColumnKeys = []
|
loadingColumnKeys = [],
|
||||||
|
draggedKey = null,
|
||||||
|
dropTargetKey = null,
|
||||||
|
onColumnDragStart,
|
||||||
|
onColumnDragOver,
|
||||||
|
onColumnDrop,
|
||||||
|
onColumnDragEnd
|
||||||
}) => {
|
}) => {
|
||||||
const showSkeleton = skeletonColumnCount > 0
|
const showSkeleton = skeletonColumnCount > 0
|
||||||
const loadingColumnKeySet = new Set(loadingColumnKeys)
|
const loadingColumnKeySet = new Set(loadingColumnKeys)
|
||||||
@ -25,6 +31,10 @@ const ObjectKanbanHeader = ({
|
|||||||
<div
|
<div
|
||||||
key={`skeleton-header-${index}`}
|
key={`skeleton-header-${index}`}
|
||||||
className='objectKanbanHeaderCell'
|
className='objectKanbanHeaderCell'
|
||||||
|
style={{
|
||||||
|
width: KANBAN_DEFAULT_COLUMN_WIDTH,
|
||||||
|
flex: `0 0 ${KANBAN_DEFAULT_COLUMN_WIDTH}px`
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Skeleton.Input
|
<Skeleton.Input
|
||||||
active
|
active
|
||||||
@ -33,17 +43,33 @@ const ObjectKanbanHeader = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
: categoryValues.map((categoryValue) => {
|
: columns.map(({ key: columnKey, value: categoryValue, width }) => {
|
||||||
const columnKey = getCategoryValueKey(categoryValue)
|
|
||||||
const showColumnLoading =
|
const showColumnLoading =
|
||||||
lazyLoading || loadingColumnKeySet.has(columnKey)
|
lazyLoading || loadingColumnKeySet.has(columnKey)
|
||||||
|
const isDragging = draggedKey === columnKey
|
||||||
|
const isDropTarget =
|
||||||
|
dropTargetKey === columnKey && draggedKey !== columnKey
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
key={columnKey}
|
key={columnKey}
|
||||||
className='objectKanbanHeaderCell'
|
className={[
|
||||||
|
'objectKanbanHeaderCell',
|
||||||
|
isDragging ? 'objectKanbanHeaderCell--dragging' : '',
|
||||||
|
isDropTarget ? 'objectKanbanHeaderCell--dropTarget' : ''
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
align='center'
|
align='center'
|
||||||
justify='space-between'
|
justify='space-between'
|
||||||
gap={8}
|
gap={8}
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
flex: `0 0 ${width}px`
|
||||||
|
}}
|
||||||
|
onDragOver={(event) =>
|
||||||
|
onColumnDragOver?.(event, columnKey)
|
||||||
|
}
|
||||||
|
onDrop={(event) => onColumnDrop?.(event, columnKey)}
|
||||||
>
|
>
|
||||||
{categoryPropertyDef ? (
|
{categoryPropertyDef ? (
|
||||||
<ObjectProperty
|
<ObjectProperty
|
||||||
@ -64,12 +90,28 @@ const ObjectKanbanHeader = ({
|
|||||||
name={categoryProperty}
|
name={categoryProperty}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{showColumnLoading && (
|
<Flex
|
||||||
<LoadingOutlined
|
align='center'
|
||||||
spin
|
gap={4}
|
||||||
style={{ flexShrink: 0, marginRight: 6 }}
|
className='objectKanbanHeaderCellActions'
|
||||||
/>
|
>
|
||||||
)}
|
{showColumnLoading && (
|
||||||
|
<LoadingOutlined spin style={{ flexShrink: 0 }} />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className='objectKanbanHeaderDragHandle overview-drag-handle'
|
||||||
|
draggable
|
||||||
|
title='Drag to reorder column'
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
|
onDragStart={(event) =>
|
||||||
|
onColumnDragStart?.(event, columnKey)
|
||||||
|
}
|
||||||
|
onDragEnd={onColumnDragEnd}
|
||||||
|
>
|
||||||
|
<HolderOutlined />
|
||||||
|
</span>
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -82,13 +124,25 @@ const ObjectKanbanHeader = ({
|
|||||||
ObjectKanbanHeader.displayName = 'ObjectKanbanHeader'
|
ObjectKanbanHeader.displayName = 'ObjectKanbanHeader'
|
||||||
|
|
||||||
ObjectKanbanHeader.propTypes = {
|
ObjectKanbanHeader.propTypes = {
|
||||||
categoryValues: PropTypes.array.isRequired,
|
columns: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
key: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.any,
|
||||||
|
width: PropTypes.number.isRequired
|
||||||
|
})
|
||||||
|
),
|
||||||
categoryProperty: PropTypes.string.isRequired,
|
categoryProperty: PropTypes.string.isRequired,
|
||||||
categoryPropertyDef: PropTypes.object,
|
categoryPropertyDef: PropTypes.object,
|
||||||
trackRef: PropTypes.object,
|
trackRef: PropTypes.object,
|
||||||
skeletonColumnCount: PropTypes.number,
|
skeletonColumnCount: PropTypes.number,
|
||||||
lazyLoading: PropTypes.bool,
|
lazyLoading: PropTypes.bool,
|
||||||
loadingColumnKeys: PropTypes.arrayOf(PropTypes.string)
|
loadingColumnKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
draggedKey: PropTypes.string,
|
||||||
|
dropTargetKey: PropTypes.string,
|
||||||
|
onColumnDragStart: PropTypes.func,
|
||||||
|
onColumnDragOver: PropTypes.func,
|
||||||
|
onColumnDrop: PropTypes.func,
|
||||||
|
onColumnDragEnd: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectKanbanHeader
|
export default ObjectKanbanHeader
|
||||||
|
|||||||
@ -2082,6 +2082,16 @@ const ObjectTable = forwardRef(
|
|||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
rowActions={rowActions}
|
rowActions={rowActions}
|
||||||
renderActions={renderActions}
|
renderActions={renderActions}
|
||||||
|
columns={viewMode.settings?.columns}
|
||||||
|
onColumnsChange={(columns) => {
|
||||||
|
objectListView?.setViewMode?.({
|
||||||
|
...viewMode,
|
||||||
|
settings: {
|
||||||
|
...viewMode.settings,
|
||||||
|
columns
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import GridIcon from '../../Icons/GridIcon'
|
|||||||
import ListIcon from '../../Icons/ListIcon'
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import KanbanIcon from '../../Icons/KanbanIcon'
|
import KanbanIcon from '../../Icons/KanbanIcon'
|
||||||
import SettingsIcon from '../../Icons/SettingsIcon'
|
import SettingsIcon from '../../Icons/SettingsIcon'
|
||||||
|
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
import {
|
import {
|
||||||
getViewModeType,
|
getViewModeType,
|
||||||
@ -67,16 +68,22 @@ const ObjectTableViewButton = ({
|
|||||||
|
|
||||||
const handleTypeChange = (nextType) => {
|
const handleTypeChange = (nextType) => {
|
||||||
if (nextType === 'kanban') {
|
if (nextType === 'kanban') {
|
||||||
setViewMode({
|
const categoryProperty =
|
||||||
|
normalizedViewMode.type === 'kanban'
|
||||||
|
? normalizedViewMode.settings?.categoryProperty ||
|
||||||
|
defaultCategoryProperty
|
||||||
|
: defaultCategoryProperty
|
||||||
|
const nextMode = {
|
||||||
type: 'kanban',
|
type: 'kanban',
|
||||||
settings: {
|
settings: { categoryProperty }
|
||||||
categoryProperty:
|
}
|
||||||
normalizedViewMode.type === 'kanban'
|
if (
|
||||||
? normalizedViewMode.settings?.categoryProperty ||
|
normalizedViewMode.type === 'kanban' &&
|
||||||
defaultCategoryProperty
|
Array.isArray(normalizedViewMode.settings?.columns)
|
||||||
: defaultCategoryProperty
|
) {
|
||||||
}
|
nextMode.settings.columns = normalizedViewMode.settings.columns
|
||||||
})
|
}
|
||||||
|
setViewMode(nextMode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,25 +175,46 @@ const ObjectTableViewButton = ({
|
|||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Modal
|
<Modal
|
||||||
title='Kanban settings'
|
|
||||||
open={settingsOpen}
|
open={settingsOpen}
|
||||||
onCancel={() => setSettingsOpen(false)}
|
onCancel={() => setSettingsOpen(false)}
|
||||||
onOk={() => setSettingsOpen(false)}
|
destroyOnHidden
|
||||||
destroyOnClose
|
focusTriggerAfterClose={false}
|
||||||
|
footer={null}
|
||||||
|
centered
|
||||||
|
closeIcon={null}
|
||||||
|
getContainer={() => document.body}
|
||||||
|
width={520}
|
||||||
>
|
>
|
||||||
<Select
|
<Flex vertical gap='middle' onMouseDown={(event) => event.stopPropagation()}>
|
||||||
style={{ width: '100%' }}
|
<Flex gap='middle'>
|
||||||
placeholder='Select category property'
|
<InfoCircleIcon />
|
||||||
value={
|
<Text strong>Kanban settings</Text>
|
||||||
normalizedViewMode.settings?.categoryProperty ||
|
</Flex>
|
||||||
defaultCategoryProperty
|
<Text>
|
||||||
}
|
Select the property used to group cards into columns:
|
||||||
onChange={handleCategoryPropertyChange}
|
</Text>
|
||||||
options={categoryProperties.map((property) => ({
|
<Select
|
||||||
value: property.name,
|
style={{ width: '100%' }}
|
||||||
label: property.label || property.name
|
placeholder='Select category property'
|
||||||
}))}
|
value={
|
||||||
/>
|
normalizedViewMode.settings?.categoryProperty ||
|
||||||
|
defaultCategoryProperty
|
||||||
|
}
|
||||||
|
onChange={handleCategoryPropertyChange}
|
||||||
|
options={categoryProperties.map((property) => ({
|
||||||
|
value: property.name,
|
||||||
|
label: property.label || property.name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<Flex justify='end' gap='small'>
|
||||||
|
<Button type='default' onClick={() => setSettingsOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type='primary' onClick={() => setSettingsOpen(false)}>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,6 +3,9 @@ export const DEFAULT_VIEW_MODE = { type: 'list' }
|
|||||||
/** Property types that can be used as kanban column categories. */
|
/** Property types that can be used as kanban column categories. */
|
||||||
export const KANBAN_CATEGORY_PROPERTY_TYPES = ['state', 'tags']
|
export const KANBAN_CATEGORY_PROPERTY_TYPES = ['state', 'tags']
|
||||||
|
|
||||||
|
export const KANBAN_DEFAULT_COLUMN_WIDTH = 360
|
||||||
|
export const KANBAN_MIN_COLUMN_WIDTH = 200
|
||||||
|
|
||||||
export const isKanbanCategoryProperty = (property) =>
|
export const isKanbanCategoryProperty = (property) =>
|
||||||
KANBAN_CATEGORY_PROPERTY_TYPES.includes(property?.type)
|
KANBAN_CATEGORY_PROPERTY_TYPES.includes(property?.type)
|
||||||
|
|
||||||
@ -32,7 +35,12 @@ export const toCategoryFilterValue = (value) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getCategoryValueKey = (value) => {
|
export const getCategoryValueKey = (value) => {
|
||||||
if (value && typeof value === 'object' && !Array.isArray(value) && value.type != null) {
|
if (
|
||||||
|
value &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
!Array.isArray(value) &&
|
||||||
|
value.type != null
|
||||||
|
) {
|
||||||
return String(value.type)
|
return String(value.type)
|
||||||
}
|
}
|
||||||
if (value != null) return String(value)
|
if (value != null) return String(value)
|
||||||
@ -56,3 +64,56 @@ export const getCategoryColumnKeys = (value) => {
|
|||||||
}
|
}
|
||||||
return [getCategoryValueKey(value)]
|
return [getCategoryValueKey(value)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge fetched category values with persisted kanban column order/widths.
|
||||||
|
* Unknown stored columns are dropped; new values are appended with the default width.
|
||||||
|
*/
|
||||||
|
export const resolveKanbanColumns = (
|
||||||
|
categoryValues = [],
|
||||||
|
storedColumns = []
|
||||||
|
) => {
|
||||||
|
const byKey = new Map(
|
||||||
|
categoryValues.map((value) => [getCategoryValueKey(value), value])
|
||||||
|
)
|
||||||
|
const seen = new Set()
|
||||||
|
const ordered = []
|
||||||
|
|
||||||
|
for (const column of storedColumns || []) {
|
||||||
|
if (column == null || column.value == null) continue
|
||||||
|
const key = String(column.value)
|
||||||
|
if (!byKey.has(key) || seen.has(key)) continue
|
||||||
|
const width =
|
||||||
|
typeof column.width === 'number' && column.width > 0
|
||||||
|
? column.width
|
||||||
|
: KANBAN_DEFAULT_COLUMN_WIDTH
|
||||||
|
ordered.push({
|
||||||
|
key,
|
||||||
|
value: byKey.get(key),
|
||||||
|
width: Math.max(KANBAN_MIN_COLUMN_WIDTH, width)
|
||||||
|
})
|
||||||
|
seen.add(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const value of categoryValues) {
|
||||||
|
const key = getCategoryValueKey(value)
|
||||||
|
if (seen.has(key)) continue
|
||||||
|
ordered.push({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
width: KANBAN_DEFAULT_COLUMN_WIDTH
|
||||||
|
})
|
||||||
|
seen.add(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toKanbanColumnsConfig = (columns) =>
|
||||||
|
(columns || []).map(({ key, width }) => ({
|
||||||
|
value: key,
|
||||||
|
width:
|
||||||
|
typeof width === 'number' && width > 0
|
||||||
|
? Math.max(KANBAN_MIN_COLUMN_WIDTH, width)
|
||||||
|
: KANBAN_DEFAULT_COLUMN_WIDTH
|
||||||
|
}))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user