From 5e6b4213ab97b4742fade0dcc2570f395ad3ee87 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Thu, 3 Sep 2026 14:27:34 +0100 Subject: [PATCH] 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. --- assets/stylesheets/App.css | 96 ++++- .../Dashboard/common/ObjectCard.jsx | 2 +- .../Dashboard/common/ObjectKanban.jsx | 337 +++++++++++++++--- .../Dashboard/common/ObjectKanbanColumn.jsx | 31 +- .../Dashboard/common/ObjectKanbanHeader.jsx | 84 ++++- .../Dashboard/common/ObjectTable.jsx | 10 + .../common/ObjectTableViewButton.jsx | 78 ++-- .../Dashboard/common/viewModeUtils.js | 63 +++- 8 files changed, 593 insertions(+), 108 deletions(-) diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index 416e29e5..05275861 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -1520,14 +1520,35 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { } .objectKanbanHeaderCell { - flex: 0 0 360px; - min-width: 360px; + flex: 0 0 auto; + min-width: 200px; flex-shrink: 0; padding: 8px 8px; border: 1px solid var(--ant-color-border-secondary); border-bottom: 1px solid var(--color-descriptions-border); border-radius: 12px 12px 0 0; 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 { @@ -1563,13 +1584,80 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { .objectKanbanColumn, .objectKanbanColumnSkeleton { - flex: 0 0 360px; - min-width: 360px; + position: relative; + flex: 0 0 auto; + min-width: 200px; min-height: 0; border-top: none; 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 { border-radius: 0 0 12px 12px; overflow: hidden; diff --git a/src/components/Dashboard/common/ObjectCard.jsx b/src/components/Dashboard/common/ObjectCard.jsx index ad9a1c74..53fb31bf 100644 --- a/src/components/Dashboard/common/ObjectCard.jsx +++ b/src/components/Dashboard/common/ObjectCard.jsx @@ -98,7 +98,7 @@ const ObjectCard = ({ return ( diff --git a/src/components/Dashboard/common/ObjectKanban.jsx b/src/components/Dashboard/common/ObjectKanban.jsx index eb8a549e..7de270e8 100644 --- a/src/components/Dashboard/common/ObjectKanban.jsx +++ b/src/components/Dashboard/common/ObjectKanban.jsx @@ -17,13 +17,31 @@ import ObjectKanbanColumn from './ObjectKanbanColumn' import ObjectKanbanHeader from './ObjectKanbanHeader' import ScrollBox from './ScrollBox' 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' -const KANBAN_COLUMN_WIDTH = 360 +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( @@ -86,45 +104,60 @@ const ObjectKanbanSkeletonColumns = ({ model, modelProperties, visibleColumns, - keyPrefix = 'skeleton' -}) => ( -
-
- - {Array.from({ length: columnCount }).map((_, columnIndex) => ( -
-
- {cardCount > 0 && ( - - {Array.from({ length: cardCount }).map((_, cardIndex) => ( - - ))} - - )} + keyPrefix = 'skeleton', + columns = null +}) => { + const skeletonColumns = + columns?.length > 0 + ? columns + : Array.from({ length: columnCount }).map((_, columnIndex) => ({ + key: `${keyPrefix}-${columnIndex}`, + width: KANBAN_COLUMN_WIDTH + })) + + return ( +
+
+ + {skeletonColumns.map((column, columnIndex) => ( +
+
+ {cardCount > 0 && ( + + {Array.from({ length: cardCount }).map((_, cardIndex) => ( + + ))} + + )} +
-
- ))} - + ))} + +
-
-) + ) +} ObjectKanbanSkeletonColumns.propTypes = { columnCount: PropTypes.number.isRequired, @@ -133,7 +166,8 @@ ObjectKanbanSkeletonColumns.propTypes = { model: PropTypes.object.isRequired, modelProperties: PropTypes.array.isRequired, visibleColumns: PropTypes.object, - keyPrefix: PropTypes.string + keyPrefix: PropTypes.string, + columns: PropTypes.array } const ObjectKanban = forwardRef( @@ -151,7 +185,9 @@ const ObjectKanban = forwardRef( visibleColumns = {}, isEditing = false, rowActions = [], - renderActions + renderActions, + columns: storedColumns = [], + onColumnsChange }, ref ) => { @@ -175,6 +211,11 @@ const ObjectKanban = forwardRef( 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) @@ -622,6 +663,168 @@ const ObjectKanban = forwardRef( [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 ( @@ -645,7 +848,7 @@ const ObjectKanban = forwardRef(
{!showSkeleton && ( @@ -663,9 +872,8 @@ const ObjectKanban = forwardRef( scrollableNodeProps={{ ref: setScrollContainerRef }} > - {categoryValues.map((categoryValue) => { - const columnKey = getCategoryValueKey(categoryValue) - return ( + {displayColumns.map( + ({ key: columnKey, value: categoryValue, width }) => ( { @@ -689,9 +897,14 @@ const ObjectKanban = forwardRef( isEditing={isEditing} rowActions={rowActions} renderActions={renderActions} + width={width} + isResizing={resizingKey === columnKey} + onResizeStart={(event) => + handleColumnResizeStart(event, columnKey) + } /> ) - })} + )}
@@ -712,17 +925,18 @@ const ObjectKanban = forwardRef( ref={skeletonTrackRef} > - {categoryValues.map((categoryValue) => { - const columnKey = getCategoryValueKey(categoryValue) - return ( -
-
-
- ) - })} + {displayColumns.map(({ key: columnKey, width }) => ( +
+
+
+ ))}
@@ -750,7 +964,14 @@ ObjectKanban.propTypes = { visibleColumns: PropTypes.object, isEditing: PropTypes.bool, 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 diff --git a/src/components/Dashboard/common/ObjectKanbanColumn.jsx b/src/components/Dashboard/common/ObjectKanbanColumn.jsx index fe0def8d..d5d845c1 100644 --- a/src/components/Dashboard/common/ObjectKanbanColumn.jsx +++ b/src/components/Dashboard/common/ObjectKanbanColumn.jsx @@ -11,10 +11,11 @@ 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' -import { toCategoryFilterValue } from './viewModeUtils' +import { KANBAN_MIN_COLUMN_WIDTH, toCategoryFilterValue } from './viewModeUtils' const SCROLL_THRESHOLD = 50 @@ -40,7 +41,10 @@ const ObjectKanbanColumn = forwardRef( rowActions = [], lazyLoading = false, renderActions, - scrollElement = null + scrollElement = null, + width, + isResizing = false, + onResizeStart }, ref ) => { @@ -545,8 +549,13 @@ const ObjectKanbanColumn = forwardRef( tableData ]) + const columnStyle = + typeof width === 'number' + ? { width, flex: `0 0 ${width}px`, minWidth: KANBAN_MIN_COLUMN_WIDTH } + : undefined + return ( - +
+ {onResizeStart && ( +
onResizeStart(event)} + /> + )} ) } @@ -610,7 +630,10 @@ ObjectKanbanColumn.propTypes = { rowActions: PropTypes.array, lazyLoading: PropTypes.bool, renderActions: PropTypes.func, - scrollElement: PropTypes.object + scrollElement: PropTypes.object, + width: PropTypes.number, + isResizing: PropTypes.bool, + onResizeStart: PropTypes.func } export default ObjectKanbanColumn diff --git a/src/components/Dashboard/common/ObjectKanbanHeader.jsx b/src/components/Dashboard/common/ObjectKanbanHeader.jsx index 34b6bb6b..9e5638c4 100644 --- a/src/components/Dashboard/common/ObjectKanbanHeader.jsx +++ b/src/components/Dashboard/common/ObjectKanbanHeader.jsx @@ -1,17 +1,23 @@ import { Flex, Skeleton } from 'antd' import PropTypes from 'prop-types' +import { HolderOutlined, LoadingOutlined } from '@ant-design/icons' import ObjectProperty from './ObjectProperty' -import { getCategoryValueKey } from './viewModeUtils' -import { LoadingOutlined } from '@ant-design/icons' +import { KANBAN_DEFAULT_COLUMN_WIDTH } from './viewModeUtils' const ObjectKanbanHeader = ({ - categoryValues, + columns = [], categoryProperty, categoryPropertyDef, trackRef, skeletonColumnCount = 0, lazyLoading = false, - loadingColumnKeys = [] + loadingColumnKeys = [], + draggedKey = null, + dropTargetKey = null, + onColumnDragStart, + onColumnDragOver, + onColumnDrop, + onColumnDragEnd }) => { const showSkeleton = skeletonColumnCount > 0 const loadingColumnKeySet = new Set(loadingColumnKeys) @@ -25,6 +31,10 @@ const ObjectKanbanHeader = ({
)) - : categoryValues.map((categoryValue) => { - const columnKey = getCategoryValueKey(categoryValue) + : columns.map(({ key: columnKey, value: categoryValue, width }) => { const showColumnLoading = lazyLoading || loadingColumnKeySet.has(columnKey) + const isDragging = draggedKey === columnKey + const isDropTarget = + dropTargetKey === columnKey && draggedKey !== columnKey return ( + onColumnDragOver?.(event, columnKey) + } + onDrop={(event) => onColumnDrop?.(event, columnKey)} > {categoryPropertyDef ? ( ) : null} - {showColumnLoading && ( - - )} + + {showColumnLoading && ( + + )} + event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onDragStart={(event) => + onColumnDragStart?.(event, columnKey) + } + onDragEnd={onColumnDragEnd} + > + + + ) })} @@ -82,13 +124,25 @@ const ObjectKanbanHeader = ({ ObjectKanbanHeader.displayName = 'ObjectKanbanHeader' 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, categoryPropertyDef: PropTypes.object, trackRef: PropTypes.object, skeletonColumnCount: PropTypes.number, 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 diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx index ebd84357..173f5ea5 100644 --- a/src/components/Dashboard/common/ObjectTable.jsx +++ b/src/components/Dashboard/common/ObjectTable.jsx @@ -2082,6 +2082,16 @@ const ObjectTable = forwardRef( isEditing={isEditing} rowActions={rowActions} renderActions={renderActions} + columns={viewMode.settings?.columns} + onColumnsChange={(columns) => { + objectListView?.setViewMode?.({ + ...viewMode, + settings: { + ...viewMode.settings, + columns + } + }) + }} />
diff --git a/src/components/Dashboard/common/ObjectTableViewButton.jsx b/src/components/Dashboard/common/ObjectTableViewButton.jsx index 960ad54c..1781d5db 100644 --- a/src/components/Dashboard/common/ObjectTableViewButton.jsx +++ b/src/components/Dashboard/common/ObjectTableViewButton.jsx @@ -15,6 +15,7 @@ import GridIcon from '../../Icons/GridIcon' import ListIcon from '../../Icons/ListIcon' import KanbanIcon from '../../Icons/KanbanIcon' import SettingsIcon from '../../Icons/SettingsIcon' +import InfoCircleIcon from '../../Icons/InfoCircleIcon' import { getModelByName } from '../../../database/ObjectModels' import { getViewModeType, @@ -67,16 +68,22 @@ const ObjectTableViewButton = ({ const handleTypeChange = (nextType) => { if (nextType === 'kanban') { - setViewMode({ + const categoryProperty = + normalizedViewMode.type === 'kanban' + ? normalizedViewMode.settings?.categoryProperty || + defaultCategoryProperty + : defaultCategoryProperty + const nextMode = { type: 'kanban', - settings: { - categoryProperty: - normalizedViewMode.type === 'kanban' - ? normalizedViewMode.settings?.categoryProperty || - defaultCategoryProperty - : defaultCategoryProperty - } - }) + settings: { categoryProperty } + } + if ( + normalizedViewMode.type === 'kanban' && + Array.isArray(normalizedViewMode.settings?.columns) + ) { + nextMode.settings.columns = normalizedViewMode.settings.columns + } + setViewMode(nextMode) return } @@ -168,25 +175,46 @@ const ObjectTableViewButton = ({ /> setSettingsOpen(false)} - onOk={() => setSettingsOpen(false)} - destroyOnClose + destroyOnHidden + focusTriggerAfterClose={false} + footer={null} + centered + closeIcon={null} + getContainer={() => document.body} + width={520} > - ({ + value: property.name, + label: property.label || property.name + }))} + /> + + + + +
) diff --git a/src/components/Dashboard/common/viewModeUtils.js b/src/components/Dashboard/common/viewModeUtils.js index 9234b17d..6a8512a3 100644 --- a/src/components/Dashboard/common/viewModeUtils.js +++ b/src/components/Dashboard/common/viewModeUtils.js @@ -3,6 +3,9 @@ export const DEFAULT_VIEW_MODE = { type: 'list' } /** Property types that can be used as kanban column categories. */ 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) => KANBAN_CATEGORY_PROPERTY_TYPES.includes(property?.type) @@ -32,7 +35,12 @@ export const toCategoryFilterValue = (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) } if (value != null) return String(value) @@ -56,3 +64,56 @@ export const getCategoryColumnKeys = (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 + }))